Regex injection

This commit is contained in:
edvraa
2021-04-16 23:19:29 +03:00
parent 578ce1e512
commit 29e320627f
7 changed files with 285 additions and 0 deletions

View File

@@ -0,0 +1,38 @@
package com.example.demo;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class DemoApplication {
@GetMapping("/string1")
public String string1(@RequestParam(value = "input", defaultValue = "test") String input,
@RequestParam(value = "pattern", defaultValue = ".*") String pattern) {
// BAD: Unsanitized user input is used to construct a regular expression
if (input.matches("^" + pattern + "=.*$"))
return "match!";
return "doesn't match!";
}
@GetMapping("/string2")
public String string2(@RequestParam(value = "input", defaultValue = "test") String input,
@RequestParam(value = "pattern", defaultValue = ".*") String pattern) {
// GOOD: User input is sanitized before constructing the regex
if (input.matches("^" + escapeSpecialRegexChars(pattern) + "=.*$"))
return "match!";
return "doesn't match!";
}
Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]");
String escapeSpecialRegexChars(String str) {
return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0");
}
}

View File

@@ -0,0 +1,48 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>
<p>
Constructing a regular expression with unsanitized user input is dangerous as a malicious user may
be able to modify the meaning of the expression. In particular, such a user may be able to provide
a regular expression fragment that takes exponential time in the worst case, and use that to
perform a Denial of Service attack.
</p>
</overview>
<recommendation>
<p>
Before embedding user input into a regular expression, use a sanitization function
to escape meta-characters that have special meaning.
</p>
</recommendation>
<example>
<p>
The following example shows a HTTP request parameter that is used to construct a regular expression:
</p>
<sample src="RegexInjection.java" />
<p>
In the first case the user-provided regex is not escaped.
If a malicious user provides a regex that has exponential worst case performance,
then this could lead to a Denial of Service.
</p>
<p>
In the second case, the user input is escaped using <code>escapeSpecialRegexChars</code> before being included
in the regular expression. This ensures that the user cannot insert characters which have a special
meaning in regular expressions.
</p>
</example>
<references>
<li>
OWASP:
<a href="https://www.owasp.org/index.php/Regular_expression_Denial_of_Service_-_ReDoS">Regular expression Denial of Service - ReDoS</a>.
</li>
<li>
Wikipedia: <a href="https://en.wikipedia.org/wiki/ReDoS">ReDoS</a>.
</li>
</references>
</qhelp>

View File

@@ -0,0 +1,72 @@
/**
* @name Regular expression injection
* @description User input should not be used in regular expressions without first being sanitized,
* otherwise a malicious user may be able to provide a regex that could require
* exponential time on certain inputs.
* @kind path-problem
* @problem.severity error
* @precision high
* @id java/regex-injection
* @tags security
* external/cwe/cwe-730
* external/cwe/cwe-400
*/
import java
import semmle.code.java.dataflow.FlowSources
import semmle.code.java.dataflow.TaintTracking
import DataFlow::PathGraph
class RegexSink extends DataFlow::ExprNode {
RegexSink() {
exists(MethodAccess ma, Method m | m = ma.getMethod() |
(
ma.getArgument(0) = this.asExpr() and
(
m.getDeclaringType().hasQualifiedName("java.lang", "String") and
(
m.hasName("matches") or
m.hasName("split") or
m.hasName("replaceFirst") or
m.hasName("replaceAll")
)
or
m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and
(
m.hasName("compile") or
m.hasName("matches")
)
)
)
)
}
}
abstract class Sanitizer extends DataFlow::ExprNode { }
class RegExpSanitizationCall extends Sanitizer {
RegExpSanitizationCall() {
exists(string calleeName, string sanitize, string regexp |
calleeName = this.asExpr().(Call).getCallee().getName() and
sanitize = "(?:escape|saniti[sz]e)" and
regexp = "regexp?"
|
calleeName.regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + ".*)")
)
}
}
class RegexInjectionConfiguration extends TaintTracking::Configuration {
RegexInjectionConfiguration() { this = "RegexInjectionConfiguration" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) { sink instanceof RegexSink }
override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer }
}
from DataFlow::PathNode source, DataFlow::PathNode sink, RegexInjectionConfiguration c
where c.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "$@ is user controlled.", source.getNode(),
"This regular expression pattern"

View File

@@ -0,0 +1,39 @@
edges
| RegexInjection.java:11:22:11:52 | getParameter(...) : String | RegexInjection.java:14:26:14:47 | ... + ... |
| RegexInjection.java:18:22:18:52 | getParameter(...) : String | RegexInjection.java:21:24:21:30 | pattern |
| RegexInjection.java:25:22:25:52 | getParameter(...) : String | RegexInjection.java:28:31:28:37 | pattern |
| RegexInjection.java:32:22:32:52 | getParameter(...) : String | RegexInjection.java:35:29:35:35 | pattern |
| RegexInjection.java:39:22:39:52 | getParameter(...) : String | RegexInjection.java:42:34:42:40 | pattern |
| RegexInjection.java:49:22:49:52 | getParameter(...) : String | RegexInjection.java:52:28:52:34 | pattern |
| RegexInjection.java:56:22:56:52 | getParameter(...) : String | RegexInjection.java:59:28:59:34 | pattern |
| RegexInjection.java:63:22:63:52 | getParameter(...) : String | RegexInjection.java:66:36:66:42 | pattern : String |
| RegexInjection.java:66:32:66:43 | foo(...) : String | RegexInjection.java:66:26:66:52 | ... + ... |
| RegexInjection.java:66:36:66:42 | pattern : String | RegexInjection.java:66:32:66:43 | foo(...) : String |
nodes
| RegexInjection.java:11:22:11:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:14:26:14:47 | ... + ... | semmle.label | ... + ... |
| RegexInjection.java:18:22:18:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:21:24:21:30 | pattern | semmle.label | pattern |
| RegexInjection.java:25:22:25:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:28:31:28:37 | pattern | semmle.label | pattern |
| RegexInjection.java:32:22:32:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:35:29:35:35 | pattern | semmle.label | pattern |
| RegexInjection.java:39:22:39:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:42:34:42:40 | pattern | semmle.label | pattern |
| RegexInjection.java:49:22:49:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:52:28:52:34 | pattern | semmle.label | pattern |
| RegexInjection.java:56:22:56:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:59:28:59:34 | pattern | semmle.label | pattern |
| RegexInjection.java:63:22:63:52 | getParameter(...) : String | semmle.label | getParameter(...) : String |
| RegexInjection.java:66:26:66:52 | ... + ... | semmle.label | ... + ... |
| RegexInjection.java:66:32:66:43 | foo(...) : String | semmle.label | foo(...) : String |
| RegexInjection.java:66:36:66:42 | pattern : String | semmle.label | pattern : String |
#select
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |
| 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 |

View File

@@ -0,0 +1,86 @@
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 RegexInjection extends HttpServlet {
public boolean string1(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return input.matches("^" + pattern + "=.*$"); // BAD
}
public boolean string2(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return input.split(pattern).length > 0; // BAD
}
public boolean string3(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return input.replaceFirst(pattern, "").length() > 0; // BAD
}
public boolean string4(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return input.replaceAll(pattern, "").length() > 0; // BAD
}
public boolean pattern1(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
Pattern pt = Pattern.compile(pattern);
Matcher matcher = pt.matcher(input);
return matcher.find(); // BAD
}
public boolean pattern2(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return Pattern.compile(pattern).matcher(input).matches(); // BAD
}
public boolean pattern3(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return Pattern.matches(pattern, input); // BAD
}
public boolean pattern4(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
return input.matches("^" + foo(pattern) + "=.*$"); // BAD
}
String foo(String str) {
return str;
}
public boolean pattern5(javax.servlet.http.HttpServletRequest request) {
String pattern = request.getParameter("pattern");
String input = request.getParameter("input");
// GOOD: User input is sanitized before constructing the regex
return input.matches("^" + escapeSpecialRegexChars(pattern) + "=.*$");
}
Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]");
String escapeSpecialRegexChars(String str) {
return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0");
}
}

View File

@@ -0,0 +1 @@
experimental/Security/CWE/CWE-730/RegexInjection.ql

View File

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