Query to detect regex dot bypass

This commit is contained in:
luchua-bc
2022-07-20 22:39:24 +00:00
parent a1d9228a66
commit 48f143e7d4
8 changed files with 408 additions and 0 deletions

View File

@@ -0,0 +1,32 @@
String PROTECTED_PATTERN = "/protected/.*";
String CONSTRAINT_PATTERN = "/protected/xyz\\.xml";
// BAD: A string with line return e.g. `/protected/%0dxyz` can bypass the path check
Pattern p = Pattern.compile(PROTECTED_PATTERN);
Matcher m = p.matcher(path);
// GOOD: A string with line return e.g. `/protected/%0dxyz` cannot bypass the path check
Pattern p = Pattern.compile(PROTECTED_PATTERN, Pattern.DOTALL);
Matcher m = p.matcher(path);
// GOOD: Only a specific path can pass the validation
Pattern p = Pattern.compile(CONSTRAINT_PATTERN);
Matcher m = p.matcher(path);
if (m.matches()) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
boolean matches = path.matches(PROTECTED_PATTERN);
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
boolean matches = Pattern.matches(PROTECTED_PATTERN, path);
if (matches) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}

View File

@@ -0,0 +1,40 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>By default, "dot" (<code>.</code>) in regular expressions matches all characters except newline characters <code>\n</code> and
<code>\r</code>. Regular expressions containing a dot can be bypassed with the characters \r(%0a) , \n(%0d) when the default regex
matching implementations of Java are used. When regular expressions serve to match protected resource patterns to grant access
to protected application resources, attackers can gain access to unauthorized paths.</p>
</overview>
<recommendation>
<p>To guard against unauthorized access, it is advisable to properly specify regex patterns for validating user input. The Java
Pattern Matcher API <code>Pattern.compile(PATTERN, Pattern.DOTALL)</code> with the <code>DOTALL</code> flag set can be adopted
to address this vulnerability.</p>
</recommendation>
<example>
<p>The following examples show the bad case and the good case respectively. The <code>bad</code> methods show a regex pattern allowing
bypass. In the <code>good</code> methods, it is shown how to solve this problem by either specifying the regex pattern correctly or
use the Java API that can detect new line characters.
</p>
<sample src="DotRegex.java" />
</example>
<references>
<li>Lay0us1:
<a href="https://github.com/Lay0us1/CVE-2022-32532">CVE 2022-22978: Authorization Bypass in RegexRequestMatcher</a>.
</li>
<li>Apache Shiro:
<a href="https://github.com/apache/shiro/commit/6bcb92e06fa588b9c7790dd01bc02135d58d3f5b">Address the RegexRequestMatcher issue in 1.9.1</a>.
</li>
<li>CVE-2022-32532:
<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>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,118 @@
/**
* @name URL matched by permissive `.` in the regular expression
* @description URL validated with permissive `.` in regex are possibly vulnerable
* to an authorization bypass.
* @kind path-problem
* @problem.severity warning
* @precision high
* @id java/permissive-dot-regex
* @tags security
* external/cwe-625
* external/cwe-863
*/
import java
import semmle.code.java.dataflow.ExternalFlow
import semmle.code.java.dataflow.FlowSources
import DataFlow::PathGraph
import Regex
/**
* `.` without a `\` prefix, which is likely not a character literal in regex
*/
class PermissiveDotStr extends StringLiteral {
PermissiveDotStr() {
// Find `.` in a string that is not prefixed with `\`
exists(string s, int i | this.getValue() = s |
s.indexOf(".") = i and
not s.charAt(i - 1) = "\\"
)
}
}
/**
* Permissive `.` in a regular expression.
*/
class PermissiveDotEx extends Expr {
PermissiveDotEx() { this instanceof PermissiveDotStr }
}
/**
* A data flow sink to construct regular expressions.
*/
class CompileRegexSink extends DataFlow::ExprNode {
CompileRegexSink() {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
(
ma.getArgument(0) = this.asExpr() and
(
m instanceof StringMatchMethod // input.matches(regexPattern)
or
m instanceof PatternCompileMethod // p = Pattern.compile(regexPattern)
or
m instanceof PatternMatchMethod // p = Pattern.matches(regexPattern, input)
)
)
)
}
}
/**
* A flow configuration for permissive dot regex.
*/
class PermissiveDotRegexConfig extends DataFlow::Configuration {
PermissiveDotRegexConfig() { this = "PermissiveDotRegex::PermissiveDotRegexConfig" }
override predicate isSource(DataFlow::Node src) { src.asExpr() instanceof PermissiveDotEx }
override predicate isSink(DataFlow::Node sink) { sink instanceof CompileRegexSink }
override predicate isBarrier(DataFlow::Node node) {
exists(
MethodAccess ma, Field f // Pattern.compile(PATTERN, Pattern.DOTALL)
|
ma.getMethod() instanceof PatternCompileMethod and
ma.getArgument(1) = f.getAnAccess() and
f.hasName("DOTALL") and
f.getDeclaringType() instanceof Pattern and
node.asExpr() = ma.getArgument(0)
)
}
}
/**
* A taint-tracking configuration for untrusted user input used to match regular expressions.
*/
class MatchRegexConfiguration extends TaintTracking::Configuration {
MatchRegexConfiguration() { this = "PermissiveDotRegex::MatchRegexConfiguration" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof MatchRegexSink }
}
from
DataFlow::PathNode source, DataFlow::PathNode sink, MatchRegexConfiguration conf,
DataFlow::PathNode source2, DataFlow::PathNode sink2, PermissiveDotRegexConfig conf2
where
conf.hasFlowPath(source, sink) and
conf2.hasFlowPath(source2, sink2) and
exists(MethodAccess ma | ma.getArgument(0) = sink2.getNode().asExpr() |
// input.matches(regexPattern)
ma.getMethod() instanceof StringMatchMethod and
ma.getQualifier() = sink.getNode().asExpr()
or
// p = Pattern.compile(regexPattern); p.matcher(input)
ma.getMethod() instanceof PatternCompileMethod and
exists(MethodAccess pma |
pma.getMethod() instanceof PatternMatcherMethod and
sink.getNode().asExpr() = pma.getArgument(0) and
DataFlow::localExprFlow(ma, pma.getQualifier())
)
or
// p = Pattern.matches(regexPattern, input)
ma.getMethod() instanceof PatternMatchMethod and
sink.getNode().asExpr() = ma.getArgument(1)
)
select sink.getNode(), source, sink, "Potentially authentication bypass due to $@.",
source.getNode(), "user-provided value"

View File

@@ -0,0 +1,98 @@
/** Provides methods related to regular expression matching. */
import java
private import semmle.code.java.dataflow.ExternalFlow
private import semmle.code.java.dataflow.FlowSources
private import semmle.code.java.dataflow.TaintTracking2
/**
* The class `Pattern` for pattern match.
*/
class Pattern extends RefType {
Pattern() { this.hasQualifiedName("java.util.regex", "Pattern") }
}
/**
* The method `compile` for `Pattern`.
*/
class PatternCompileMethod extends Method {
PatternCompileMethod() {
this.getDeclaringType().getASupertype*() instanceof Pattern and
this.hasName("compile")
}
}
/**
* The method `matches` for `Pattern`.
*/
class PatternMatchMethod extends Method {
PatternMatchMethod() {
this.getDeclaringType().getASupertype*() instanceof Pattern and
this.hasName("matches")
}
}
/**
* The method `matcher` for `Pattern`.
*/
class PatternMatcherMethod extends Method {
PatternMatcherMethod() {
this.getDeclaringType().getASupertype*() instanceof Pattern and
this.hasName("matcher")
}
}
/**
* The method `matches` for `String`.
*/
class StringMatchMethod extends Method {
StringMatchMethod() {
this.getDeclaringType().getASupertype*() instanceof TypeString and
this.hasName("matches")
}
}
abstract class MatchRegexSink extends DataFlow::ExprNode { }
/**
* A data flow sink to string match regular expressions.
*/
class StringMatchRegexSink extends MatchRegexSink {
StringMatchRegexSink() {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
(
m instanceof StringMatchMethod and
ma.getQualifier() = this.asExpr()
)
)
}
}
/**
* A data flow sink to `pattern.matches` regular expressions.
*/
class PatternMatchRegexSink extends MatchRegexSink {
PatternMatchRegexSink() {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
(
m instanceof PatternMatchMethod and
ma.getArgument(1) = this.asExpr()
)
)
}
}
/**
* A data flow sink to `pattern.matcher` match regular expressions.
*/
class PatternMatcherRegexSink extends MatchRegexSink {
PatternMatcherRegexSink() {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
(
m instanceof PatternMatcherMethod and
ma.getArgument(0) = this.asExpr()
)
)
}
}

View File

@@ -0,0 +1,87 @@
import java.io.IOException;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.ServletException;
public class DotRegexServlet extends HttpServlet {
private static final String PROTECTED_PATTERN = "/protected/.*";
private static final String CONSTRAINT_PATTERN = "/protected/xyz\\.xml";
@Override
// BAD: A string with line return e.g. `/protected/%0dxyz` can bypass the path check
protected void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String source = request.getPathInfo();
Pattern p = Pattern.compile(PROTECTED_PATTERN);
Matcher m = p.matcher(source);
if (m.matches()) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}
}
// GOOD: A string with line return e.g. `/protected/%0dxyz` cannot bypass the path check
protected void doGet2(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String source = request.getPathInfo();
Pattern p = Pattern.compile(PROTECTED_PATTERN, Pattern.DOTALL);
Matcher m = p.matcher(source);
if (m.matches()) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}
}
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
protected void doGet3(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String source = request.getPathInfo();
boolean matches = source.matches(PROTECTED_PATTERN);
if (matches) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}
}
// BAD: A string with line return e.g. `/protected/%0axyz` can bypass the path check
protected void doGet4(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String source = request.getPathInfo();
boolean matches = Pattern.matches(PROTECTED_PATTERN, source);
if (matches) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}
}
// GOOD: Only a specific path can pass the validation
protected void doGet5(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
String source = request.getPathInfo();
Pattern p = Pattern.compile(CONSTRAINT_PATTERN);
Matcher m = p.matcher(source);
if (m.matches()) {
// Protected page - check access token and redirect to login page
} else {
// Not protected page - render content
}
}
}

View File

@@ -0,0 +1,31 @@
edges
| DotRegexServlet.java:11:30:11:46 | PROTECTED_PATTERN : String | DotRegexServlet.java:20:31:20:47 | PROTECTED_PATTERN |
| DotRegexServlet.java:11:30:11:46 | PROTECTED_PATTERN : String | DotRegexServlet.java:50:36:50:52 | PROTECTED_PATTERN |
| DotRegexServlet.java:11:30:11:46 | PROTECTED_PATTERN : String | DotRegexServlet.java:64:37:64:53 | PROTECTED_PATTERN |
| DotRegexServlet.java:11:50:11:64 | "/protected/.*" : String | DotRegexServlet.java:11:30:11:46 | PROTECTED_PATTERN : String |
| DotRegexServlet.java:18:19:18:39 | getPathInfo(...) : String | DotRegexServlet.java:21:25:21:30 | source |
| DotRegexServlet.java:33:19:33:39 | getPathInfo(...) : String | DotRegexServlet.java:36:25:36:30 | source |
| DotRegexServlet.java:48:19:48:39 | getPathInfo(...) : String | DotRegexServlet.java:50:21:50:26 | source |
| DotRegexServlet.java:62:19:62:39 | getPathInfo(...) : String | DotRegexServlet.java:64:56:64:61 | source |
| DotRegexServlet.java:76:19:76:39 | getPathInfo(...) : String | DotRegexServlet.java:79:25:79:30 | source |
nodes
| DotRegexServlet.java:11:30:11:46 | PROTECTED_PATTERN : String | semmle.label | PROTECTED_PATTERN : String |
| DotRegexServlet.java:11:50:11:64 | "/protected/.*" : String | semmle.label | "/protected/.*" : String |
| DotRegexServlet.java:18:19:18:39 | getPathInfo(...) : String | semmle.label | getPathInfo(...) : String |
| DotRegexServlet.java:20:31:20:47 | PROTECTED_PATTERN | semmle.label | PROTECTED_PATTERN |
| DotRegexServlet.java:21:25:21:30 | source | semmle.label | source |
| DotRegexServlet.java:33:19:33:39 | getPathInfo(...) : String | semmle.label | getPathInfo(...) : String |
| DotRegexServlet.java:36:25:36:30 | source | semmle.label | source |
| DotRegexServlet.java:48:19:48:39 | getPathInfo(...) : String | semmle.label | getPathInfo(...) : String |
| DotRegexServlet.java:50:21:50:26 | source | semmle.label | source |
| DotRegexServlet.java:50:36:50:52 | PROTECTED_PATTERN | semmle.label | PROTECTED_PATTERN |
| DotRegexServlet.java:62:19:62:39 | getPathInfo(...) : String | semmle.label | getPathInfo(...) : String |
| DotRegexServlet.java:64:37:64:53 | PROTECTED_PATTERN | semmle.label | PROTECTED_PATTERN |
| DotRegexServlet.java:64:56:64:61 | source | semmle.label | source |
| DotRegexServlet.java:76:19:76:39 | getPathInfo(...) : String | semmle.label | getPathInfo(...) : String |
| DotRegexServlet.java:79:25:79:30 | source | semmle.label | source |
subpaths
#select
| DotRegexServlet.java:21:25:21:30 | source | DotRegexServlet.java:18:19:18:39 | getPathInfo(...) : String | DotRegexServlet.java:21:25:21:30 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:18:19:18:39 | getPathInfo(...) | user-provided value |
| DotRegexServlet.java:50:21:50:26 | source | DotRegexServlet.java:48:19:48:39 | getPathInfo(...) : String | DotRegexServlet.java:50:21:50:26 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:48:19:48:39 | getPathInfo(...) | user-provided value |
| DotRegexServlet.java:64:56:64:61 | source | DotRegexServlet.java:62:19:62:39 | getPathInfo(...) : String | DotRegexServlet.java:64:56:64:61 | source | Potentially authentication bypass due to $@. | DotRegexServlet.java:62:19:62:39 | getPathInfo(...) | user-provided value |

View File

@@ -0,0 +1 @@
experimental/Security/CWE/CWE-625/PermissiveDotRegex.ql

View File

@@ -0,0 +1 @@
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4