Skip to content

Commit 2ec53bf

Browse files
authored
Merge pull request #9873 from luchua-bc/java/permissive-dot-regex
Java: CWE-625 Query to detect regex dot bypass
2 parents e4853d0 + e2e8798 commit 2ec53bf

File tree

13 files changed

+724
-7
lines changed

13 files changed

+724
-7
lines changed

java/ql/src/experimental/Security/CWE/CWE-601/SpringUrlRedirect.ql

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
*/
1212

1313
import java
14-
import SpringUrlRedirect
14+
import experimental.semmle.code.java.security.SpringUrlRedirect
1515
import semmle.code.java.dataflow.FlowSources
1616
import semmle.code.java.controlflow.Guards
1717
import DataFlow::PathGraph
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
String PROTECTED_PATTERN = "/protected/.*";
2+
String CONSTRAINT_PATTERN = "/protected/xyz\\.xml";
3+
4+
// BAD: A string with line return e.g. `/protected/%0dxyz` can bypass the path check
5+
Pattern p = Pattern.compile(PROTECTED_PATTERN);
6+
Matcher m = p.matcher(path);
7+
8+
// GOOD: A string with line return e.g. `/protected/%0dxyz` cannot bypass the path check
9+
Pattern p = Pattern.compile(PROTECTED_PATTERN, Pattern.DOTALL);
10+
Matcher m = p.matcher(path);
11+
12+
// GOOD: Only a specific path can pass the validation
13+
Pattern p = Pattern.compile(CONSTRAINT_PATTERN);
14+
Matcher m = p.matcher(path);
15+
16+
if (m.matches()) {
17+
// Protected page - check access token and redirect to login page
18+
} else {
19+
// Not protected page - render content
20+
}
21+
22+
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
23+
boolean matches = path.matches(PROTECTED_PATTERN);
24+
25+
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
26+
boolean matches = Pattern.matches(PROTECTED_PATTERN, path);
27+
28+
if (matches) {
29+
// Protected page - check access token and redirect to login page
30+
} else {
31+
// Not protected page - render content
32+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>By default, "dot" (<code>.</code>) in regular expressions matches all characters except newline characters <code>\n</code> and
8+
<code>\r</code>. Regular expressions containing a dot can be bypassed with the characters \r(%0a) , \n(%0d) when the default regex
9+
matching implementations of Java are used. When regular expressions serve to match protected resource patterns to grant access
10+
to protected application resources, attackers can gain access to unauthorized paths.</p>
11+
</overview>
12+
13+
<recommendation>
14+
<p>To guard against unauthorized access, it is advisable to properly specify regex patterns for validating user input. The Java
15+
Pattern Matcher API <code>Pattern.compile(PATTERN, Pattern.DOTALL)</code> with the <code>DOTALL</code> flag set can be adopted
16+
to address this vulnerability.</p>
17+
</recommendation>
18+
19+
<example>
20+
<p>The following examples show the bad case and the good case respectively. The <code>bad</code> methods show a regex pattern allowing
21+
bypass. In the <code>good</code> methods, it is shown how to solve this problem by either specifying the regex pattern correctly or
22+
use the Java API that can detect new line characters.
23+
</p>
24+
25+
<sample src="DotRegex.java" />
26+
</example>
27+
28+
<references>
29+
<li>Lay0us1:
30+
<a href="https://github.com/Lay0us1/CVE-2022-32532">CVE 2022-22978: Authorization Bypass in RegexRequestMatcher</a>.
31+
</li>
32+
<li>Apache Shiro:
33+
<a href="https://github.com/apache/shiro/commit/6bcb92e06fa588b9c7790dd01bc02135d58d3f5b">Address the RegexRequestMatcher issue in 1.9.1</a>.
34+
</li>
35+
<li>CVE-2022-32532:
36+
<a href="https://cve.mitre.org/cgi-bin/cvename.cgi?name=CVE-2022-32532">Applications using RegExPatternMatcher with "." in the regular expression are possibly vulnerable to an authorization bypass</a>.
37+
</li>
38+
</references>
39+
40+
</qhelp>
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
/**
2+
* @name URL matched by permissive `.` in the regular expression
3+
* @description URL validated with permissive `.` in regex are possibly vulnerable
4+
* to an authorization bypass.
5+
* @kind path-problem
6+
* @problem.severity warning
7+
* @precision high
8+
* @id java/permissive-dot-regex
9+
* @tags security
10+
* external/cwe-625
11+
* external/cwe-863
12+
*/
13+
14+
import java
15+
import semmle.code.java.dataflow.FlowSources
16+
import DataFlow::PathGraph
17+
import PermissiveDotRegexQuery
18+
19+
from DataFlow::PathNode source, DataFlow::PathNode sink, MatchRegexConfiguration conf
20+
where conf.hasFlowPath(source, sink)
21+
select sink.getNode(), source, sink, "Potentially authentication bypass due to $@.",
22+
source.getNode(), "user-provided value"
Lines changed: 218 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,218 @@
1+
/** Provides classes related to security-centered regular expression matching. */
2+
3+
import java
4+
private import semmle.code.java.dataflow.ExternalFlow
5+
private import semmle.code.java.dataflow.FlowSources
6+
import experimental.semmle.code.java.security.SpringUrlRedirect
7+
import semmle.code.java.controlflow.Guards
8+
import semmle.code.java.security.UrlRedirect
9+
import Regex
10+
11+
/** A string that ends with `.*` not prefixed with `\`. */
12+
class PermissiveDotStr extends StringLiteral {
13+
PermissiveDotStr() {
14+
exists(string s, int i | this.getValue() = s |
15+
s.indexOf(".*") = i and
16+
not s.charAt(i - 1) = "\\" and
17+
s.length() = i + 2
18+
)
19+
}
20+
}
21+
22+
/** Source model of remote flow source with servlets. */
23+
private class GetServletUriSource extends SourceModelCsv {
24+
override predicate row(string row) {
25+
row =
26+
[
27+
"javax.servlet.http;HttpServletRequest;false;getPathInfo;();;ReturnValue;uri-path;manual",
28+
"javax.servlet.http;HttpServletRequest;false;getPathTranslated;();;ReturnValue;uri-path;manual",
29+
"javax.servlet.http;HttpServletRequest;false;getRequestURI;();;ReturnValue;uri-path;manual",
30+
"javax.servlet.http;HttpServletRequest;false;getRequestURL;();;ReturnValue;uri-path;manual",
31+
"javax.servlet.http;HttpServletRequest;false;getServletPath;();;ReturnValue;uri-path;manual"
32+
]
33+
}
34+
}
35+
36+
/** Sink of servlet dispatcher. */
37+
private class UrlDispatchSink extends UrlRedirectSink {
38+
UrlDispatchSink() {
39+
exists(MethodAccess ma |
40+
ma.getMethod() instanceof RequestDispatchMethod and
41+
this.asExpr() = ma.getQualifier()
42+
)
43+
}
44+
}
45+
46+
/** The `doFilter` method of `javax.servlet.FilterChain`. */
47+
private class ServletFilterMethod extends Method {
48+
ServletFilterMethod() {
49+
this.getDeclaringType().getASupertype*().hasQualifiedName("javax.servlet", "FilterChain") and
50+
this.hasName("doFilter")
51+
}
52+
}
53+
54+
/** Sink of servlet filter. */
55+
private class UrlFilterSink extends UrlRedirectSink {
56+
UrlFilterSink() {
57+
exists(MethodAccess ma |
58+
ma.getMethod() instanceof ServletFilterMethod and
59+
this.asExpr() = ma.getQualifier()
60+
)
61+
}
62+
}
63+
64+
/** A Spring framework annotation indicating remote uri user input. */
65+
class SpringUriInputAnnotation extends Annotation {
66+
SpringUriInputAnnotation() {
67+
this.getType()
68+
.hasQualifiedName("org.springframework.web.bind.annotation",
69+
["PathVariable", "RequestParam"])
70+
}
71+
}
72+
73+
class SpringUriInputParameterSource extends DataFlow::Node {
74+
SpringUriInputParameterSource() {
75+
this.asParameter() =
76+
any(SpringRequestMappingParameter srmp |
77+
srmp.getAnAnnotation() instanceof SpringUriInputAnnotation
78+
)
79+
}
80+
}
81+
82+
/**
83+
* A data flow sink to construct regular expressions.
84+
*/
85+
class CompileRegexSink extends DataFlow::ExprNode {
86+
CompileRegexSink() {
87+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
88+
(
89+
ma.getArgument(0) = this.asExpr() and
90+
(
91+
m instanceof StringMatchMethod // input.matches(regexPattern)
92+
or
93+
m instanceof PatternCompileMethod // p = Pattern.compile(regexPattern)
94+
or
95+
m instanceof PatternMatchMethod // p = Pattern.matches(regexPattern, input)
96+
)
97+
)
98+
)
99+
}
100+
}
101+
102+
/**
103+
* A flow configuration for permissive dot regex.
104+
*/
105+
class PermissiveDotRegexConfig extends DataFlow2::Configuration {
106+
PermissiveDotRegexConfig() { this = "PermissiveDotRegex::PermissiveDotRegexConfig" }
107+
108+
override predicate isSource(DataFlow2::Node src) { src.asExpr() instanceof PermissiveDotStr }
109+
110+
override predicate isSink(DataFlow2::Node sink) { sink instanceof CompileRegexSink }
111+
112+
override predicate isBarrier(DataFlow2::Node node) {
113+
exists(
114+
MethodAccess ma, Field f // Pattern.compile(PATTERN, Pattern.DOTALL)
115+
|
116+
ma.getMethod() instanceof PatternCompileMethod and
117+
ma.getArgument(1) = f.getAnAccess() and
118+
f.hasName("DOTALL") and
119+
f.getDeclaringType() instanceof Pattern and
120+
node.asExpr() = ma.getArgument(0)
121+
)
122+
}
123+
}
124+
125+
/**
126+
* A taint-tracking configuration for untrusted user input used to match regular expressions.
127+
*/
128+
class MatchRegexConfiguration extends TaintTracking::Configuration {
129+
MatchRegexConfiguration() { this = "PermissiveDotRegex::MatchRegexConfiguration" }
130+
131+
override predicate isSource(DataFlow::Node source) {
132+
sourceNode(source, "uri-path") or // Servlet uri source
133+
source instanceof SpringUriInputParameterSource // Spring uri source
134+
}
135+
136+
override predicate isSink(DataFlow::Node sink) {
137+
sink instanceof MatchRegexSink and
138+
exists(
139+
Guard guard, Expr se, Expr ce // used in a condition to control url redirect, which is a typical security enforcement
140+
|
141+
(
142+
sink.asExpr() = ce.(MethodAccess).getQualifier() or
143+
sink.asExpr() = ce.(MethodAccess).getAnArgument() or
144+
sink.asExpr() = ce
145+
) and
146+
(
147+
DataFlow::localExprFlow(ce, guard.(MethodAccess).getQualifier()) or
148+
DataFlow::localExprFlow(ce, guard.(MethodAccess).getAnArgument())
149+
) and
150+
(
151+
DataFlow::exprNode(se) instanceof UrlRedirectSink or
152+
DataFlow::exprNode(se) instanceof SpringUrlRedirectSink
153+
) and
154+
guard.controls(se.getBasicBlock(), true)
155+
) and
156+
exists(MethodAccess ma | any(PermissiveDotRegexConfig conf2).hasFlowToExpr(ma.getArgument(0)) |
157+
// input.matches(regexPattern)
158+
ma.getMethod() instanceof StringMatchMethod and
159+
ma.getQualifier() = sink.asExpr()
160+
or
161+
// p = Pattern.compile(regexPattern); p.matcher(input)
162+
ma.getMethod() instanceof PatternCompileMethod and
163+
exists(MethodAccess pma |
164+
pma.getMethod() instanceof PatternMatcherMethod and
165+
sink.asExpr() = pma.getArgument(0) and
166+
DataFlow::localExprFlow(ma, pma.getQualifier())
167+
)
168+
or
169+
// p = Pattern.matches(regexPattern, input)
170+
ma.getMethod() instanceof PatternMatchMethod and
171+
sink.asExpr() = ma.getArgument(1)
172+
)
173+
}
174+
}
175+
176+
abstract class MatchRegexSink extends DataFlow::ExprNode { }
177+
178+
/**
179+
* A data flow sink to string match regular expressions.
180+
*/
181+
class StringMatchRegexSink extends MatchRegexSink {
182+
StringMatchRegexSink() {
183+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
184+
(
185+
m instanceof StringMatchMethod and
186+
ma.getQualifier() = this.asExpr()
187+
)
188+
)
189+
}
190+
}
191+
192+
/**
193+
* A data flow sink to `pattern.matches` regular expressions.
194+
*/
195+
class PatternMatchRegexSink extends MatchRegexSink {
196+
PatternMatchRegexSink() {
197+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
198+
(
199+
m instanceof PatternMatchMethod and
200+
ma.getArgument(1) = this.asExpr()
201+
)
202+
)
203+
}
204+
}
205+
206+
/**
207+
* A data flow sink to `pattern.matcher` match regular expressions.
208+
*/
209+
class PatternMatcherRegexSink extends MatchRegexSink {
210+
PatternMatcherRegexSink() {
211+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
212+
(
213+
m instanceof PatternMatcherMethod and
214+
ma.getArgument(0) = this.asExpr()
215+
)
216+
)
217+
}
218+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/** Provides methods related to regular expression matching. */
2+
3+
import java
4+
5+
/**
6+
* The class `Pattern` for pattern match.
7+
*/
8+
class Pattern extends RefType {
9+
Pattern() { this.hasQualifiedName("java.util.regex", "Pattern") }
10+
}
11+
12+
/**
13+
* The method `compile` for `Pattern`.
14+
*/
15+
class PatternCompileMethod extends Method {
16+
PatternCompileMethod() {
17+
this.getDeclaringType().getASupertype*() instanceof Pattern and
18+
this.hasName("compile")
19+
}
20+
}
21+
22+
/**
23+
* The method `matches` for `Pattern`.
24+
*/
25+
class PatternMatchMethod extends Method {
26+
PatternMatchMethod() {
27+
this.getDeclaringType().getASupertype*() instanceof Pattern and
28+
this.hasName("matches")
29+
}
30+
}
31+
32+
/**
33+
* The method `matcher` for `Pattern`.
34+
*/
35+
class PatternMatcherMethod extends Method {
36+
PatternMatcherMethod() {
37+
this.getDeclaringType().getASupertype*() instanceof Pattern and
38+
this.hasName("matcher")
39+
}
40+
}
41+
42+
/**
43+
* The method `matches` for `String`.
44+
*/
45+
class StringMatchMethod extends Method {
46+
StringMatchMethod() {
47+
this.getDeclaringType().getASupertype*() instanceof TypeString and
48+
this.hasName("matches")
49+
}
50+
}

0 commit comments

Comments
 (0)