spring mvc url matching disables suffix access operations

  • 2021-11-01 03:08:27
  • OfStack

spring mvc url Match Disable Suffix Access

In spring mvc, the default access url can be accessed with any suffix name

For example: You want to access the/login, but you can access the/login. do/login. action/login. json

Generally speaking, it may not affect, but for permission control, it is serious.

There are usually two ideas for permission control:

1) Weak authority control

Allow all url to pass, and only control the privileges of some important url. This method is relatively simple, and it does not need to configure all url resources, only important resources.

2) Strong authority control

By default, all url requests are prohibited from passing, and only authorized resources are open. In this way, all url resources are controlled. In the system, you need to sort out all requests, or all url resources in a 1 directory. This method has strict safety control and troublesome operation, but it is relatively safe.

If the second method is used, the access policy of spring mvc above has no impact on security.

However, if the first security policy is used, there will be great security risks.

For example: We control access to the/login, but our default resources except the/login are not subject to permission control, then an attacker can use the/login. do/login. xxx to access our resources.

After spring 3.1, url finds the corresponding processing method. Step 1, directly call RequestMappingHandlerMapping to find the corresponding processing method. Step 2, call RequestMappingHandlerAdapter to process

We can see in RequestMappingHandlerMapping


/**
 * Whether to use suffix pattern match for registered file extensions only
 * when matching patterns to requests.
 * <p>If enabled, a controller method mapped to "/users" also matches to
 * "/users.json" assuming ".json" is a file extension registered with the
 * provided {@link #setContentNegotiationManager(ContentNegotiationManager)
 * contentNegotiationManager}. This can be useful for allowing only specific
 * URL extensions to be used as well as in cases where a "." in the URL path
 * can lead to ambiguous interpretation of path variable content, (e.g. given
 * "/users/{user}" and incoming URLs such as "/users/john.j.joe" and
 * "/users/john.j.joe.json").
 * <p>If enabled, this flag also enables
 * {@link #setUseSuffixPatternMatch(boolean) useSuffixPatternMatch}. The
 * default value is {@code false}.
 */
public void setUseRegisteredSuffixPatternMatch(boolean useRegisteredSuffixPatternMatch) {
   this.useRegisteredSuffixPatternMatch = useRegisteredSuffixPatternMatch;
   this.useSuffixPatternMatch = (useRegisteredSuffixPatternMatch || this.useSuffixPatternMatch);
}

So how to configure it?


  <mvc:annotation-driven>
    <mvc:path-matching suffix-pattern="false" />
  </mvc:annotation-driven>

Whether to use suffix pattern matching when matching patterns, the default value is true. So if you want to access /login, you can't access it through /login.*.

spring mvc Request url Suffix

When RequestMappingInfoHandlerMapping processes an http request, if the request url has a suffix, if the exact match @ RequestMapping method cannot be found.

Then, remove the suffix, and then. * matches, so that all 1 can match. For example, if there is one @ RequestMapping ("/rest"), then in the case of an exact match, only the/rest request will be matched.

But if my front end sends a request like the/rest. abcdef and no mapping like @ RequestMapping ("/rest. abcdef") is configured, then @ RequestMapping ("/rest") will take effect.

What about the principle? The processing chain looks like this:


at org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getMatchingPattern(PatternsRequestCondition.java:254)
at org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getMatchingPatterns(PatternsRequestCondition.java:230)
at org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.getMatchingCondition(PatternsRequestCondition.java:210)
at org.springframework.web.servlet.mvc.method.RequestMappingInfo.getMatchingCondition(RequestMappingInfo.java:214)
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getMatchingMapping(RequestMappingInfoHandlerMapping.java:79)
at org.springframework.web.servlet.mvc.method.RequestMappingInfoHandlerMapping.getMatchingMapping(RequestMappingInfoHandlerMapping.java:56)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.addMatchingMappings(AbstractHandlerMethodMapping.java:358)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lookupHandlerMethod(AbstractHandlerMethodMapping.java:328)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:299)
at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.getHandlerInternal(AbstractHandlerMethodMapping.java:57)
at org.springframework.web.servlet.handler.AbstractHandlerMapping.getHandler(AbstractHandlerMapping.java:299)
at org.springframework.web.servlet.DispatcherServlet.getHandler(DispatcherServlet.java:1104)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:916)

The key is PatternsRequestCondition, specifically this method:


AbstractHandlerMethodMapping  Adj. getHandlerInternal : 
    protected HandlerMethod getHandlerInternal(HttpServletRequest request) throws Exception {
        String lookupPath = this.getUrlPathHelper().getLookupPathForRequest(request);
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Looking up handler method for path " + lookupPath);
        }
        HandlerMethod handlerMethod = this.lookupHandlerMethod(lookupPath, request);//  Here is the key. It looks for it. If it finds it, it will find it. If it can't find it, it won't look for it again 
        if (this.logger.isDebugEnabled()) {
            if (handlerMethod != null) {
                this.logger.debug("Returning handler method [" + handlerMethod + "]");
            } else {
                this.logger.debug("Did not find handler method for [" + lookupPath + "]");
            }
        }
        return handlerMethod != null ? handlerMethod.createWithResolvedBean() : null;
    }
    
    
    protected HandlerMethod lookupHandlerMethod(String lookupPath, HttpServletRequest request) throws Exception {
        List<AbstractHandlerMethodMapping<T>.Match> matches = new ArrayList();
        List<T> directPathMatches = (List)this.urlMap.get(lookupPath); // directPathMatches ,   Direct matching,   It can also be said to be   Exact matching 
        if (directPathMatches != null) {
            this.addMatchingMappings(directPathMatches, matches, request);//  If you can match exactly,   Will come in here 
        }
        if (matches.isEmpty()) {
            this.addMatchingMappings(this.handlerMethods.keySet(), matches, request);//  If you can't match exactly,   Will come in here 
        }
        if (!matches.isEmpty()) {
            Comparator<AbstractHandlerMethodMapping<T>.Match> comparator = new AbstractHandlerMethodMapping.MatchComparator(this.getMappingComparator(request));
            Collections.sort(matches, comparator);
            if (this.logger.isTraceEnabled()) {
                this.logger.trace("Found " + matches.size() + " matching mapping(s) for [" + lookupPath + "] : " + matches);
            }
            AbstractHandlerMethodMapping<T>.Match bestMatch = (AbstractHandlerMethodMapping.Match)matches.get(0);
            if (matches.size() > 1) {
                AbstractHandlerMethodMapping<T>.Match secondBestMatch = (AbstractHandlerMethodMapping.Match)matches.get(1);
                if (comparator.compare(bestMatch, secondBestMatch) == 0) {
                    Method m1 = bestMatch.handlerMethod.getMethod();
                    Method m2 = secondBestMatch.handlerMethod.getMethod();
                    throw new IllegalStateException("Ambiguous handler methods mapped for HTTP path '" + request.getRequestURL() + "': {" + m1 + ", " + m2 + "}");
                }
            }
            this.handleMatch(bestMatch.mapping, lookupPath, request);
            return bestMatch.handlerMethod;
        } else {
            return this.handleNoMatch(this.handlerMethods.keySet(), lookupPath, request);
        }
    }
    
    public List<String> getMatchingPatterns(String lookupPath) {
        List<String> matches = new ArrayList();
        Iterator var3 = this.patterns.iterator();
        while(var3.hasNext()) {
            String pattern = (String)var3.next(); // pattern  Yes  @RequestMapping  Mapping provided 
            String match = this.getMatchingPattern(pattern, lookupPath); //  lookupPath + .*   Can be matched after pattern ,   Then it is not empty 
            if (match != null) {
                matches.add(match);//  In the case of suffixes,  .*  Posterior 
            }
        }
        Collections.sort(matches, this.pathMatcher.getPatternComparator(lookupPath));
        return matches;
    }
     The most important thing is here  getMatchingPatterns  : 
    private String getMatchingPattern(String pattern, String lookupPath) {
        if (pattern.equals(lookupPath)) {
            return pattern;
        } else {
            if (this.useSuffixPatternMatch) {
                if (!this.fileExtensions.isEmpty() && lookupPath.indexOf(46) != -1) {
                    Iterator var5 = this.fileExtensions.iterator();
                    while(var5.hasNext()) {
                        String extension = (String)var5.next();
                        if (this.pathMatcher.match(pattern + extension, lookupPath)) {
                            return pattern + extension;
                        }
                    }
                } else {
                    boolean hasSuffix = pattern.indexOf(46) != -1;
                    if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
                        return pattern + ".*"; //  The point is here 
                    }
                }
            }
            if (this.pathMatcher.match(pattern, lookupPath)) {
                return pattern;
            } else {
                return this.useTrailingSlashMatch && !pattern.endsWith("/") && this.pathMatcher.match(pattern + "/", lookupPath) ? pattern + "/" : null;
            }
        }
    }

For AbstractUrlHandlerMapping, if you can't match it, you can't match it, and you won't match it after +.*.

The key method is this:


protected Object lookupHandler(String urlPath, HttpServletRequest request) throws Exception {
        Object handler = this.handlerMap.get(urlPath);
        if (handler != null) {
            if (handler instanceof String) {
                String handlerName = (String)handler;
                handler = this.getApplicationContext().getBean(handlerName);
            }
            this.validateHandler(handler, request);
            return this.buildPathExposingHandler(handler, urlPath, urlPath, (Map)null);
        } else {
            List<String> matchingPatterns = new ArrayList();
            Iterator var5 = this.handlerMap.keySet().iterator();
            while(var5.hasNext()) {
                String registeredPattern = (String)var5.next();
                if (this.getPathMatcher().match(registeredPattern, urlPath)) {
                    matchingPatterns.add(registeredPattern);
                }
            }
            String bestPatternMatch = null;
            Comparator<String> patternComparator = this.getPathMatcher().getPatternComparator(urlPath);
            if (!matchingPatterns.isEmpty()) {
                Collections.sort(matchingPatterns, patternComparator);
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Matching patterns for request [" + urlPath + "] are " + matchingPatterns);
                }
                bestPatternMatch = (String)matchingPatterns.get(0);
            }
            if (bestPatternMatch != null) {
                handler = this.handlerMap.get(bestPatternMatch);
                String pathWithinMapping;
                if (handler instanceof String) {
                    pathWithinMapping = (String)handler;
                    handler = this.getApplicationContext().getBean(pathWithinMapping);
                }
                this.validateHandler(handler, request);
                pathWithinMapping = this.getPathMatcher().extractPathWithinPattern(bestPatternMatch, urlPath);
                Map<String, String> uriTemplateVariables = new LinkedHashMap();
                Iterator var9 = matchingPatterns.iterator();
                while(var9.hasNext()) {
                    String matchingPattern = (String)var9.next();
                    if (patternComparator.compare(bestPatternMatch, matchingPattern) == 0) {
                        Map<String, String> vars = this.getPathMatcher().extractUriTemplateVariables(matchingPattern, urlPath);
                        Map<String, String> decodedVars = this.getUrlPathHelper().decodePathVariables(request, vars);
                        uriTemplateVariables.putAll(decodedVars);
                    }
                }
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("URI Template variables for request [" + urlPath + "] are " + uriTemplateVariables);
                }
                return this.buildPathExposingHandler(handler, bestPatternMatch, pathWithinMapping, uriTemplateVariables);
            } else {
                return null;
            }
        }
    }

Of course, maybe we can set up a custom PathMatcher to achieve our goal. The default is AntPathMatcher.


Related articles: