Java URL Pattern Matching Gotchas

Many security features in Java rely on endpoint pattern matching which allow for URL pattern matching bypasses if not careful. Additionally Spring MVC and Spring Security together introduces are a few gotcha’s during implementation.

Security Constraint Matching

The most basic form of authentication within Java is using the <security-constraint/> tag. The following is an example constraint restricting access to /basic endpoint using Basic auth.

<!-- Basic Security -->
<security-constraint>
    <web-resource-collection>
        <web-resource-name>baisc auth restriction</web-resource-name>
        <url-pattern>/basic</url-pattern>
    </web-resource-collection>
    …
</security-constraint>

All official Java documentation uses the url-pattern /*, though details for <url-pattern/> can be found in section 12.2 of the 3.0 servlet specification. The following details mapping test-cases which have standard Java servlets mapped to /basic

Servlet Map <url-pattern/> Request Response Code
/basic /basic* GET /basic 200
/basic /basic/ GET /basic 200
/basic /basic/* GET /basic 401
/basic /basic GET /basic 401

Wild cards do not function as expected, and only by leaving out extensions for a literal matching or using /* can servlet patterns be correctly matched.

Security Constraints with Spring MVC

However if, for example, using the <security-restraint/> function to protect Spring MVC controllers the following is observed:

MVC Map <url-pattern/> Request Response Code
/mvcpoint /mvcpoint* GET /mvcpoint 200
/mvcpoint /mvcpoint/ GET /mvcpoint 200
/mvcpoint /mvcpoint/* GET /mvcpoint. 200
/mvcpoint /mvcpoint GET /mvcpoint/ 200
/mvcpoint /mvcpoint*/* GET /mvcpoint 200

The lesson here is never use standard <security-constraint/> methods when attempting to restrict endpoints which are not servlets. We will touch on the use of the . below.

Spring Security Pattern Matching

Security interceptors used by Spring Security use ANT pattern matching. This type of matching is different from Regex. See the ANT documentation on patterns for specifics.

The following is an example of a Spring Security URL pattern constraint:

<http>
    <intercept-url pattern="/welcome*" access="ROLE_USER" />
    <http-basic />
</http>

The following is a test table showing various patterns, and their bypass:

MVC Map <intercept-url/> Request Response Code
/mvcpoint /mvcpoint* GET /mvcpoint/ 200
/mvcpoint /mvcpoint/ GET /mvcpoint 200
/mvcpoint /mvcpoint/* GET /mvcpoint 200
/mvcpoint /mvcpoint** GET /mvcpoint/ 200
/mvcpoint /mvcpoint/** GET /mvcpoint. 200
/mvcpoint /mvcpoint GET /mvcpoint. 200
/mvcpoint /mvcpoint*/* GET /mvcpoint 200
/mvcpoint /mvcpoint**/* GET /mvcpoint 200
/mvcpoint /mvcpoint*/** GET /mvcpoint. 401

The table shows that only a single spring-security pattern (*/**) is able to secure against unauthorized access. To discover why, we must dig deeper.

Spring MVC with Spring Security

The problem arises in spring-mvc and its handling of extensions. We are able to supply an incomplete extension (note the trailing . on the requests) to spring-security which results in the bypass of the rule. When the incomplete extension gets to spring-mvc however, the incomplete extension is treated as erroneous and automatically returns the original mapping.

private String getMatchingPattern(String pattern, String lookupPath) {
    if (pattern.equals(lookupPath)) {
        return pattern;
    }
    if (this.useSuffixPatternMatch) {
        if (useSmartSuffixPatternMatch(pattern, lookupPath)) {
            for (String extension : this.fileExtensions) {
                if (this.pathMatcher.match(pattern + extension, lookupPath)) {
                    return pattern + extension;
                }
            }
        }
        else {
            boolean hasSuffix = pattern.indexOf('.') != -1;
            if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
                return pattern + ".*";
            }
        }
    }
    if (this.pathMatcher.match(pattern, lookupPath)) {
        return pattern;
    }
    boolean endsWithSlash = pattern.endsWith("/");
    if (this.useTrailingSlashMatch) {
        if (!endsWithSlash && this.pathMatcher.match(pattern + "/", lookupPath)) {
            return pattern +"/";
        }
    }
    return null;
}

private boolean useSmartSuffixPatternMatch(String pattern, String lookupPath) {
    return (!this.fileExtensions.isEmpty() && lookupPath.indexOf('.') != -1) ;
}

org.springframework.web.servlet.mvc.condition.PatternsRequestCondition.java starting at line 223

The code, upon being passed to spring mvc as such

getMatchingPattern("/basic2", "/basic2.")

Will end up in the following block because of useSmartSuffixPatternMatch evaluating to false.

boolean hasSuffix = pattern.indexOf('.') != -1;
if (!hasSuffix && this.pathMatcher.match(pattern + ".*", lookupPath)) {
    return pattern + ".*";
}

This results in the return of “/basic2.*” due to the pathMatcher automatically appending .* to the pattern. In the end this function will find the correct controller mapping in spite of the added period.

Summary

  • Only protect servlets with the <security-constraint/> element
  • Always use /* when defining <security-constraint/>  patterns
  • Always use */** when defining <intercept-url/> patterns
This entry was posted in java, webapp. Bookmark the permalink.

Leave a Reply

Your email address will not be published.