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.
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:
The following is a test table showing various patterns, and their bypass:
MVC Map | 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.
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