Skip to content

Commit 48f143e

Browse files
committed
Query to detect regex dot bypass
1 parent a1d9228 commit 48f143e

File tree

8 files changed

+408
-0
lines changed

8 files changed

+408
-0
lines changed
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: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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.ExternalFlow
16+
import semmle.code.java.dataflow.FlowSources
17+
import DataFlow::PathGraph
18+
import Regex
19+
20+
/**
21+
* `.` without a `\` prefix, which is likely not a character literal in regex
22+
*/
23+
class PermissiveDotStr extends StringLiteral {
24+
PermissiveDotStr() {
25+
// Find `.` in a string that is not prefixed with `\`
26+
exists(string s, int i | this.getValue() = s |
27+
s.indexOf(".") = i and
28+
not s.charAt(i - 1) = "\\"
29+
)
30+
}
31+
}
32+
33+
/**
34+
* Permissive `.` in a regular expression.
35+
*/
36+
class PermissiveDotEx extends Expr {
37+
PermissiveDotEx() { this instanceof PermissiveDotStr }
38+
}
39+
40+
/**
41+
* A data flow sink to construct regular expressions.
42+
*/
43+
class CompileRegexSink extends DataFlow::ExprNode {
44+
CompileRegexSink() {
45+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
46+
(
47+
ma.getArgument(0) = this.asExpr() and
48+
(
49+
m instanceof StringMatchMethod // input.matches(regexPattern)
50+
or
51+
m instanceof PatternCompileMethod // p = Pattern.compile(regexPattern)
52+
or
53+
m instanceof PatternMatchMethod // p = Pattern.matches(regexPattern, input)
54+
)
55+
)
56+
)
57+
}
58+
}
59+
60+
/**
61+
* A flow configuration for permissive dot regex.
62+
*/
63+
class PermissiveDotRegexConfig extends DataFlow::Configuration {
64+
PermissiveDotRegexConfig() { this = "PermissiveDotRegex::PermissiveDotRegexConfig" }
65+
66+
override predicate isSource(DataFlow::Node src) { src.asExpr() instanceof PermissiveDotEx }
67+
68+
override predicate isSink(DataFlow::Node sink) { sink instanceof CompileRegexSink }
69+
70+
override predicate isBarrier(DataFlow::Node node) {
71+
exists(
72+
MethodAccess ma, Field f // Pattern.compile(PATTERN, Pattern.DOTALL)
73+
|
74+
ma.getMethod() instanceof PatternCompileMethod and
75+
ma.getArgument(1) = f.getAnAccess() and
76+
f.hasName("DOTALL") and
77+
f.getDeclaringType() instanceof Pattern and
78+
node.asExpr() = ma.getArgument(0)
79+
)
80+
}
81+
}
82+
83+
/**
84+
* A taint-tracking configuration for untrusted user input used to match regular expressions.
85+
*/
86+
class MatchRegexConfiguration extends TaintTracking::Configuration {
87+
MatchRegexConfiguration() { this = "PermissiveDotRegex::MatchRegexConfiguration" }
88+
89+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
90+
91+
override predicate isSink(DataFlow::Node sink) { sink instanceof MatchRegexSink }
92+
}
93+
94+
from
95+
DataFlow::PathNode source, DataFlow::PathNode sink, MatchRegexConfiguration conf,
96+
DataFlow::PathNode source2, DataFlow::PathNode sink2, PermissiveDotRegexConfig conf2
97+
where
98+
conf.hasFlowPath(source, sink) and
99+
conf2.hasFlowPath(source2, sink2) and
100+
exists(MethodAccess ma | ma.getArgument(0) = sink2.getNode().asExpr() |
101+
// input.matches(regexPattern)
102+
ma.getMethod() instanceof StringMatchMethod and
103+
ma.getQualifier() = sink.getNode().asExpr()
104+
or
105+
// p = Pattern.compile(regexPattern); p.matcher(input)
106+
ma.getMethod() instanceof PatternCompileMethod and
107+
exists(MethodAccess pma |
108+
pma.getMethod() instanceof PatternMatcherMethod and
109+
sink.getNode().asExpr() = pma.getArgument(0) and
110+
DataFlow::localExprFlow(ma, pma.getQualifier())
111+
)
112+
or
113+
// p = Pattern.matches(regexPattern, input)
114+
ma.getMethod() instanceof PatternMatchMethod and
115+
sink.getNode().asExpr() = ma.getArgument(1)
116+
)
117+
select sink.getNode(), source, sink, "Potentially authentication bypass due to $@.",
118+
source.getNode(), "user-provided value"
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
/** Provides methods related to regular expression matching. */
2+
3+
import java
4+
private import semmle.code.java.dataflow.ExternalFlow
5+
private import semmle.code.java.dataflow.FlowSources
6+
private import semmle.code.java.dataflow.TaintTracking2
7+
8+
/**
9+
* The class `Pattern` for pattern match.
10+
*/
11+
class Pattern extends RefType {
12+
Pattern() { this.hasQualifiedName("java.util.regex", "Pattern") }
13+
}
14+
15+
/**
16+
* The method `compile` for `Pattern`.
17+
*/
18+
class PatternCompileMethod extends Method {
19+
PatternCompileMethod() {
20+
this.getDeclaringType().getASupertype*() instanceof Pattern and
21+
this.hasName("compile")
22+
}
23+
}
24+
25+
/**
26+
* The method `matches` for `Pattern`.
27+
*/
28+
class PatternMatchMethod extends Method {
29+
PatternMatchMethod() {
30+
this.getDeclaringType().getASupertype*() instanceof Pattern and
31+
this.hasName("matches")
32+
}
33+
}
34+
35+
/**
36+
* The method `matcher` for `Pattern`.
37+
*/
38+
class PatternMatcherMethod extends Method {
39+
PatternMatcherMethod() {
40+
this.getDeclaringType().getASupertype*() instanceof Pattern and
41+
this.hasName("matcher")
42+
}
43+
}
44+
45+
/**
46+
* The method `matches` for `String`.
47+
*/
48+
class StringMatchMethod extends Method {
49+
StringMatchMethod() {
50+
this.getDeclaringType().getASupertype*() instanceof TypeString and
51+
this.hasName("matches")
52+
}
53+
}
54+
55+
abstract class MatchRegexSink extends DataFlow::ExprNode { }
56+
57+
/**
58+
* A data flow sink to string match regular expressions.
59+
*/
60+
class StringMatchRegexSink extends MatchRegexSink {
61+
StringMatchRegexSink() {
62+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
63+
(
64+
m instanceof StringMatchMethod and
65+
ma.getQualifier() = this.asExpr()
66+
)
67+
)
68+
}
69+
}
70+
71+
/**
72+
* A data flow sink to `pattern.matches` regular expressions.
73+
*/
74+
class PatternMatchRegexSink extends MatchRegexSink {
75+
PatternMatchRegexSink() {
76+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
77+
(
78+
m instanceof PatternMatchMethod and
79+
ma.getArgument(1) = this.asExpr()
80+
)
81+
)
82+
}
83+
}
84+
85+
/**
86+
* A data flow sink to `pattern.matcher` match regular expressions.
87+
*/
88+
class PatternMatcherRegexSink extends MatchRegexSink {
89+
PatternMatcherRegexSink() {
90+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
91+
(
92+
m instanceof PatternMatcherMethod and
93+
ma.getArgument(0) = this.asExpr()
94+
)
95+
)
96+
}
97+
}
98+
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
import java.io.IOException;
2+
import java.util.regex.Matcher;
3+
import java.util.regex.Pattern;
4+
5+
import javax.servlet.http.HttpServlet;
6+
import javax.servlet.http.HttpServletRequest;
7+
import javax.servlet.http.HttpServletResponse;
8+
import javax.servlet.ServletException;
9+
10+
public class DotRegexServlet extends HttpServlet {
11+
private static final String PROTECTED_PATTERN = "/protected/.*";
12+
private static final String CONSTRAINT_PATTERN = "/protected/xyz\\.xml";
13+
14+
@Override
15+
// BAD: A string with line return e.g. `/protected/%0dxyz` can bypass the path check
16+
protected void doGet(HttpServletRequest request, HttpServletResponse response)
17+
throws ServletException, IOException {
18+
String source = request.getPathInfo();
19+
20+
Pattern p = Pattern.compile(PROTECTED_PATTERN);
21+
Matcher m = p.matcher(source);
22+
23+
if (m.matches()) {
24+
// Protected page - check access token and redirect to login page
25+
} else {
26+
// Not protected page - render content
27+
}
28+
}
29+
30+
// GOOD: A string with line return e.g. `/protected/%0dxyz` cannot bypass the path check
31+
protected void doGet2(HttpServletRequest request, HttpServletResponse response)
32+
throws ServletException, IOException {
33+
String source = request.getPathInfo();
34+
35+
Pattern p = Pattern.compile(PROTECTED_PATTERN, Pattern.DOTALL);
36+
Matcher m = p.matcher(source);
37+
38+
if (m.matches()) {
39+
// Protected page - check access token and redirect to login page
40+
} else {
41+
// Not protected page - render content
42+
}
43+
}
44+
45+
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
46+
protected void doGet3(HttpServletRequest request, HttpServletResponse response)
47+
throws ServletException, IOException {
48+
String source = request.getPathInfo();
49+
50+
boolean matches = source.matches(PROTECTED_PATTERN);
51+
52+
if (matches) {
53+
// Protected page - check access token and redirect to login page
54+
} else {
55+
// Not protected page - render content
56+
}
57+
}
58+
59+
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
60+
protected void doGet4(HttpServletRequest request, HttpServletResponse response)
61+
throws ServletException, IOException {
62+
String source = request.getPathInfo();
63+
64+
boolean matches = Pattern.matches(PROTECTED_PATTERN, source);
65+
66+
if (matches) {
67+
// Protected page - check access token and redirect to login page
68+
} else {
69+
// Not protected page - render content
70+
}
71+
}
72+
73+
// GOOD: Only a specific path can pass the validation
74+
protected void doGet5(HttpServletRequest request, HttpServletResponse response)
75+
throws ServletException, IOException {
76+
String source = request.getPathInfo();
77+
78+
Pattern p = Pattern.compile(CONSTRAINT_PATTERN);
79+
Matcher m = p.matcher(source);
80+
81+
if (m.matches()) {
82+
// Protected page - check access token and redirect to login page
83+
} else {
84+
// Not protected page - render content
85+
}
86+
}
87+
}

0 commit comments

Comments
 (0)