mirror of
https://github.com/github/codeql.git
synced 2026-04-30 11:15:13 +02:00
Query to detect regex dot bypass
This commit is contained in:
32
java/ql/src/experimental/Security/CWE/CWE-625/DotRegex.java
Normal file
32
java/ql/src/experimental/Security/CWE/CWE-625/DotRegex.java
Normal 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
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
98
java/ql/src/experimental/Security/CWE/CWE-625/Regex.qll
Normal file
98
java/ql/src/experimental/Security/CWE/CWE-625/Regex.qll
Normal 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()
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 |
|
||||
@@ -0,0 +1 @@
|
||||
experimental/Security/CWE/CWE-625/PermissiveDotRegex.ql
|
||||
@@ -0,0 +1 @@
|
||||
//semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4
|
||||
Reference in New Issue
Block a user