Skip to content

Commit 29e3206

Browse files
author
edvraa
committed
Regex injection
1 parent 578ce1e commit 29e3206

File tree

7 files changed

+285
-0
lines changed

7 files changed

+285
-0
lines changed
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package com.example.demo;
2+
3+
import java.util.regex.Matcher;
4+
import java.util.regex.Pattern;
5+
6+
import org.springframework.web.bind.annotation.GetMapping;
7+
import org.springframework.web.bind.annotation.RequestParam;
8+
import org.springframework.web.bind.annotation.RestController;
9+
10+
@RestController
11+
public class DemoApplication {
12+
13+
@GetMapping("/string1")
14+
public String string1(@RequestParam(value = "input", defaultValue = "test") String input,
15+
@RequestParam(value = "pattern", defaultValue = ".*") String pattern) {
16+
// BAD: Unsanitized user input is used to construct a regular expression
17+
if (input.matches("^" + pattern + "=.*$"))
18+
return "match!";
19+
20+
return "doesn't match!";
21+
}
22+
23+
@GetMapping("/string2")
24+
public String string2(@RequestParam(value = "input", defaultValue = "test") String input,
25+
@RequestParam(value = "pattern", defaultValue = ".*") String pattern) {
26+
// GOOD: User input is sanitized before constructing the regex
27+
if (input.matches("^" + escapeSpecialRegexChars(pattern) + "=.*$"))
28+
return "match!";
29+
30+
return "doesn't match!";
31+
}
32+
33+
Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]");
34+
35+
String escapeSpecialRegexChars(String str) {
36+
return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0");
37+
}
38+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>
8+
Constructing a regular expression with unsanitized user input is dangerous as a malicious user may
9+
be able to modify the meaning of the expression. In particular, such a user may be able to provide
10+
a regular expression fragment that takes exponential time in the worst case, and use that to
11+
perform a Denial of Service attack.
12+
</p>
13+
</overview>
14+
15+
<recommendation>
16+
<p>
17+
Before embedding user input into a regular expression, use a sanitization function
18+
to escape meta-characters that have special meaning.
19+
</p>
20+
</recommendation>
21+
22+
<example>
23+
<p>
24+
The following example shows a HTTP request parameter that is used to construct a regular expression:
25+
</p>
26+
<sample src="RegexInjection.java" />
27+
<p>
28+
In the first case the user-provided regex is not escaped.
29+
If a malicious user provides a regex that has exponential worst case performance,
30+
then this could lead to a Denial of Service.
31+
</p>
32+
<p>
33+
In the second case, the user input is escaped using <code>escapeSpecialRegexChars</code> before being included
34+
in the regular expression. This ensures that the user cannot insert characters which have a special
35+
meaning in regular expressions.
36+
</p>
37+
</example>
38+
39+
<references>
40+
<li>
41+
OWASP:
42+
<a href="https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS">Regular expression Denial of Service - ReDoS</a>.
43+
</li>
44+
<li>
45+
Wikipedia: <a href="https://en.wikipedia.org/wiki/ReDoS">ReDoS</a>.
46+
</li>
47+
</references>
48+
</qhelp>
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* @name Regular expression injection
3+
* @description User input should not be used in regular expressions without first being sanitized,
4+
* otherwise a malicious user may be able to provide a regex that could require
5+
* exponential time on certain inputs.
6+
* @kind path-problem
7+
* @problem.severity error
8+
* @precision high
9+
* @id java/regex-injection
10+
* @tags security
11+
* external/cwe/cwe-730
12+
* external/cwe/cwe-400
13+
*/
14+
15+
import java
16+
import semmle.code.java.dataflow.FlowSources
17+
import semmle.code.java.dataflow.TaintTracking
18+
import DataFlow::PathGraph
19+
20+
class RegexSink extends DataFlow::ExprNode {
21+
RegexSink() {
22+
exists(MethodAccess ma, Method m | m = ma.getMethod() |
23+
(
24+
ma.getArgument(0) = this.asExpr() and
25+
(
26+
m.getDeclaringType().hasQualifiedName("java.lang", "String") and
27+
(
28+
m.hasName("matches") or
29+
m.hasName("split") or
30+
m.hasName("replaceFirst") or
31+
m.hasName("replaceAll")
32+
)
33+
or
34+
m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and
35+
(
36+
m.hasName("compile") or
37+
m.hasName("matches")
38+
)
39+
)
40+
)
41+
)
42+
}
43+
}
44+
45+
abstract class Sanitizer extends DataFlow::ExprNode { }
46+
47+
class RegExpSanitizationCall extends Sanitizer {
48+
RegExpSanitizationCall() {
49+
exists(string calleeName, string sanitize, string regexp |
50+
calleeName = this.asExpr().(Call).getCallee().getName() and
51+
sanitize = "(?:escape|saniti[sz]e)" and
52+
regexp = "regexp?"
53+
|
54+
calleeName.regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + ".*)")
55+
)
56+
}
57+
}
58+
59+
class RegexInjectionConfiguration extends TaintTracking::Configuration {
60+
RegexInjectionConfiguration() { this = "RegexInjectionConfiguration" }
61+
62+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
63+
64+
override predicate isSink(DataFlow::Node sink) { sink instanceof RegexSink }
65+
66+
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
67+
}
68+
69+
from DataFlow::PathNode source, DataFlow::PathNode sink, RegexInjectionConfiguration c
70+
where c.hasFlowPath(source, sink)
71+
select sink.getNode(), source, sink, "$@ is user controlled.", source.getNode(),
72+
"This regular expression pattern"
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
edges
2+
| RegexInjection.java:11:22:11:52 | getParameter(...) : String | RegexInjection.java:14:26:14:47 | ... + ... |
3+
| RegexInjection.java:18:22:18:52 | getParameter(...) : String | RegexInjection.java:21:24:21:30 | pattern |
4+
| RegexInjection.java:25:22:25:52 | getParameter(...) : String | RegexInjection.java:28:31:28:37 | pattern |
5+
| RegexInjection.java:32:22:32:52 | getParameter(...) : String | RegexInjection.java:35:29:35:35 | pattern |
6+
| RegexInjection.java:39:22:39:52 | getParameter(...) : String | RegexInjection.java:42:34:42:40 | pattern |
7+
| RegexInjection.java:49:22:49:52 | getParameter(...) : String | RegexInjection.java:52:28:52:34 | pattern |
8+
| RegexInjection.java:56:22:56:52 | getParameter(...) : String | RegexInjection.java:59:28:59:34 | pattern |
9+
| RegexInjection.java:63:22:63:52 | getParameter(...) : String | RegexInjection.java:66:36:66:42 | pattern : String |
10+
| RegexInjection.java:66:32:66:43 | foo(...) : String | RegexInjection.java:66:26:66:52 | ... + ... |
11+
| RegexInjection.java:66:36:66:42 | pattern : String | RegexInjection.java:66:32:66:43 | foo(...) : String |
12+
nodes
13+
| RegexInjection.java:11:22:11:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
14+
| RegexInjection.java:14:26:14:47 | ... + ... | semmle.label | ... + ... |
15+
| RegexInjection.java:18:22:18:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
16+
| RegexInjection.java:21:24:21:30 | pattern | semmle.label | pattern |
17+
| RegexInjection.java:25:22:25:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
18+
| RegexInjection.java:28:31:28:37 | pattern | semmle.label | pattern |
19+
| RegexInjection.java:32:22:32:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
20+
| RegexInjection.java:35:29:35:35 | pattern | semmle.label | pattern |
21+
| RegexInjection.java:39:22:39:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
22+
| RegexInjection.java:42:34:42:40 | pattern | semmle.label | pattern |
23+
| RegexInjection.java:49:22:49:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
24+
| RegexInjection.java:52:28:52:34 | pattern | semmle.label | pattern |
25+
| RegexInjection.java:56:22:56:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
26+
| RegexInjection.java:59:28:59:34 | pattern | semmle.label | pattern |
27+
| RegexInjection.java:63:22:63:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
28+
| RegexInjection.java:66:26:66:52 | ... + ... | semmle.label | ... + ... |
29+
| RegexInjection.java:66:32:66:43 | foo(...) : String | semmle.label | foo(...) : String |
30+
| RegexInjection.java:66:36:66:42 | pattern : String | semmle.label | pattern : String |
31+
#select
32+
| RegexInjection.java:14:26:14:47 | ... + ... | RegexInjection.java:11:22:11:52 | getParameter(...) : String | RegexInjection.java:14:26:14:47 | ... + ... | $@ is user controlled. | RegexInjection.java:11:22:11:52 | getParameter(...) | This regular expression pattern |
33+
| RegexInjection.java:21:24:21:30 | pattern | RegexInjection.java:18:22:18:52 | getParameter(...) : String | RegexInjection.java:21:24:21:30 | pattern | $@ is user controlled. | RegexInjection.java:18:22:18:52 | getParameter(...) | This regular expression pattern |
34+
| RegexInjection.java:28:31:28:37 | pattern | RegexInjection.java:25:22:25:52 | getParameter(...) : String | RegexInjection.java:28:31:28:37 | pattern | $@ is user controlled. | RegexInjection.java:25:22:25:52 | getParameter(...) | This regular expression pattern |
35+
| RegexInjection.java:35:29:35:35 | pattern | RegexInjection.java:32:22:32:52 | getParameter(...) : String | RegexInjection.java:35:29:35:35 | pattern | $@ is user controlled. | RegexInjection.java:32:22:32:52 | getParameter(...) | This regular expression pattern |
36+
| RegexInjection.java:42:34:42:40 | pattern | RegexInjection.java:39:22:39:52 | getParameter(...) : String | RegexInjection.java:42:34:42:40 | pattern | $@ is user controlled. | RegexInjection.java:39:22:39:52 | getParameter(...) | This regular expression pattern |
37+
| RegexInjection.java:52:28:52:34 | pattern | RegexInjection.java:49:22:49:52 | getParameter(...) : String | RegexInjection.java:52:28:52:34 | pattern | $@ is user controlled. | RegexInjection.java:49:22:49:52 | getParameter(...) | This regular expression pattern |
38+
| RegexInjection.java:59:28:59:34 | pattern | RegexInjection.java:56:22:56:52 | getParameter(...) : String | RegexInjection.java:59:28:59:34 | pattern | $@ is user controlled. | RegexInjection.java:56:22:56:52 | getParameter(...) | This regular expression pattern |
39+
| RegexInjection.java:66:26:66:52 | ... + ... | RegexInjection.java:63:22:63:52 | getParameter(...) : String | RegexInjection.java:66:26:66:52 | ... + ... | $@ is user controlled. | RegexInjection.java:63:22:63:52 | getParameter(...) | This regular expression pattern |
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
import java.util.regex.Matcher;
2+
import java.util.regex.Pattern;
3+
4+
import javax.servlet.http.HttpServlet;
5+
import javax.servlet.http.HttpServletRequest;
6+
import javax.servlet.http.HttpServletResponse;
7+
import javax.servlet.ServletException;
8+
9+
public class RegexInjection extends HttpServlet {
10+
public boolean string1(javax.servlet.http.HttpServletRequest request) {
11+
String pattern = request.getParameter("pattern");
12+
String input = request.getParameter("input");
13+
14+
return input.matches("^" + pattern + "=.*$"); // BAD
15+
}
16+
17+
public boolean string2(javax.servlet.http.HttpServletRequest request) {
18+
String pattern = request.getParameter("pattern");
19+
String input = request.getParameter("input");
20+
21+
return input.split(pattern).length > 0; // BAD
22+
}
23+
24+
public boolean string3(javax.servlet.http.HttpServletRequest request) {
25+
String pattern = request.getParameter("pattern");
26+
String input = request.getParameter("input");
27+
28+
return input.replaceFirst(pattern, "").length() > 0; // BAD
29+
}
30+
31+
public boolean string4(javax.servlet.http.HttpServletRequest request) {
32+
String pattern = request.getParameter("pattern");
33+
String input = request.getParameter("input");
34+
35+
return input.replaceAll(pattern, "").length() > 0; // BAD
36+
}
37+
38+
public boolean pattern1(javax.servlet.http.HttpServletRequest request) {
39+
String pattern = request.getParameter("pattern");
40+
String input = request.getParameter("input");
41+
42+
Pattern pt = Pattern.compile(pattern);
43+
Matcher matcher = pt.matcher(input);
44+
45+
return matcher.find(); // BAD
46+
}
47+
48+
public boolean pattern2(javax.servlet.http.HttpServletRequest request) {
49+
String pattern = request.getParameter("pattern");
50+
String input = request.getParameter("input");
51+
52+
return Pattern.compile(pattern).matcher(input).matches(); // BAD
53+
}
54+
55+
public boolean pattern3(javax.servlet.http.HttpServletRequest request) {
56+
String pattern = request.getParameter("pattern");
57+
String input = request.getParameter("input");
58+
59+
return Pattern.matches(pattern, input); // BAD
60+
}
61+
62+
public boolean pattern4(javax.servlet.http.HttpServletRequest request) {
63+
String pattern = request.getParameter("pattern");
64+
String input = request.getParameter("input");
65+
66+
return input.matches("^" + foo(pattern) + "=.*$"); // BAD
67+
}
68+
69+
String foo(String str) {
70+
return str;
71+
}
72+
73+
public boolean pattern5(javax.servlet.http.HttpServletRequest request) {
74+
String pattern = request.getParameter("pattern");
75+
String input = request.getParameter("input");
76+
77+
// GOOD: User input is sanitized before constructing the regex
78+
return input.matches("^" + escapeSpecialRegexChars(pattern) + "=.*$");
79+
}
80+
81+
Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]");
82+
83+
String escapeSpecialRegexChars(String str) {
84+
return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0");
85+
}
86+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
experimental/Security/CWE/CWE-730/RegexInjection.ql
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4

0 commit comments

Comments
 (0)