From 32b140045e81ff32ac451007f94b1c72bbdf2bad Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Sun, 23 Oct 2022 20:03:10 -0400 Subject: [PATCH 01/23] move files out of experimental --- .../Security/CWE/CWE-730/RegexInjection.java | 2 +- .../Security/CWE/CWE-730/RegexInjection.qhelp | 0 .../Security/CWE/CWE-730/RegexInjection.ql | 22 ++++++++++++++++--- .../security/CWE-730/RegexInjection.qlref | 1 - .../security/CWE-730/RegexInjection.expected | 0 .../security/CWE-730/RegexInjection.java | 0 .../security/CWE-730/RegexInjection.qlref | 1 + .../security/CWE-730/options-regexInjection} | 0 8 files changed, 21 insertions(+), 5 deletions(-) rename java/ql/src/{experimental => }/Security/CWE/CWE-730/RegexInjection.java (99%) rename java/ql/src/{experimental => }/Security/CWE/CWE-730/RegexInjection.qhelp (100%) rename java/ql/src/{experimental => }/Security/CWE/CWE-730/RegexInjection.ql (71%) delete mode 100644 java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.qlref rename java/ql/test/{experimental => }/query-tests/security/CWE-730/RegexInjection.expected (100%) rename java/ql/test/{experimental => }/query-tests/security/CWE-730/RegexInjection.java (100%) create mode 100644 java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref rename java/ql/test/{experimental/query-tests/security/CWE-730/options => query-tests/security/CWE-730/options-regexInjection} (100%) diff --git a/java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.java b/java/ql/src/Security/CWE/CWE-730/RegexInjection.java similarity index 99% rename from java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.java rename to java/ql/src/Security/CWE/CWE-730/RegexInjection.java index 387648a443e6..30b74df0a0ff 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.java +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.java @@ -35,4 +35,4 @@ public String string2(@RequestParam(value = "input", defaultValue = "test") Stri String escapeSpecialRegexChars(String str) { return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); } -} \ No newline at end of file +} diff --git a/java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.qhelp b/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp similarity index 100% rename from java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.qhelp rename to java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp diff --git a/java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.ql b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql similarity index 71% rename from java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.ql rename to java/ql/src/Security/CWE/CWE-730/RegexInjection.ql index f60e5d9070bb..94bfccd9d3e2 100644 --- a/java/ql/src/experimental/Security/CWE/CWE-730/RegexInjection.ql +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql @@ -27,19 +27,24 @@ class RegexSink extends DataFlow::ExprNode { m.getDeclaringType() instanceof TypeString and ( ma.getArgument(0) = this.asExpr() and + // TODO: confirm if more/less than the below need to be handled m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) ) or + // TODO: review Java Pattern API m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and ( ma.getArgument(0) = this.asExpr() and + // TODO: confirm if more/less than the below need to be handled m.hasName(["compile", "matches"]) ) or + // TODO: read docs about regex APIs in Java m.getDeclaringType().hasQualifiedName("org.apache.commons.lang3", "RegExUtils") and ( ma.getArgument(1) = this.asExpr() and m.getParameterType(1) instanceof TypeString and + // TODO: confirm if more/less than the below need to be handled m.hasName([ "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", "replacePattern" @@ -50,6 +55,7 @@ class RegexSink extends DataFlow::ExprNode { } } +// TODO: is this abstract class needed? Are there pre-existing sanitizer classes that can be used instead? abstract class Sanitizer extends DataFlow::ExprNode { } /** @@ -60,12 +66,12 @@ 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?" + sanitize = "(?:escape|saniti[sz]e)" and // TODO: confirm this is sufficient + regexp = "regexp?" // TODO: confirm this is sufficient | calleeName .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + - ".*)") + ".*)") // TODO: confirm this is sufficient ) } } @@ -87,3 +93,13 @@ from DataFlow::PathNode source, DataFlow::PathNode sink, RegexInjectionConfigura where c.hasFlowPath(source, sink) select sink.getNode(), source, sink, "This regular expression is constructed from a $@.", source.getNode(), "user-provided value" +// from MethodAccess ma +// where +// // ma.getMethod().hasName("startsWith") and // graphhopper +// // ma.getFile().getBaseName() = "NavigateResource.java" // graphhopper +// // ma.getMethod().hasName("substring") and // jfinal +// // ma.getFile().getBaseName() = "FileManager.java" // jfinal +// ma.getMethod().hasName("startsWith") and // roller +// ma.getFile().getBaseName() = "PageServlet.java" // roller (or RegexUtil.java) +// ProteinArraySignificanceTestJSON.java or MockRKeys.java for cbioportal +// select ma, "method access" diff --git a/java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.qlref b/java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.qlref deleted file mode 100644 index dca594b38d22..000000000000 --- a/java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -experimental/Security/CWE/CWE-730/RegexInjection.ql \ No newline at end of file diff --git a/java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.expected b/java/ql/test/query-tests/security/CWE-730/RegexInjection.expected similarity index 100% rename from java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.expected rename to java/ql/test/query-tests/security/CWE-730/RegexInjection.expected diff --git a/java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.java b/java/ql/test/query-tests/security/CWE-730/RegexInjection.java similarity index 100% rename from java/ql/test/experimental/query-tests/security/CWE-730/RegexInjection.java rename to java/ql/test/query-tests/security/CWE-730/RegexInjection.java diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref b/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref new file mode 100644 index 000000000000..36e4f927f0a7 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref @@ -0,0 +1 @@ +experimental/Security/CWE/CWE-730/RegexInjection.ql diff --git a/java/ql/test/experimental/query-tests/security/CWE-730/options b/java/ql/test/query-tests/security/CWE-730/options-regexInjection similarity index 100% rename from java/ql/test/experimental/query-tests/security/CWE-730/options rename to java/ql/test/query-tests/security/CWE-730/options-regexInjection From 25436fe55569f274da218a8d0235831d9828c995 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Mon, 24 Oct 2022 13:46:24 -0400 Subject: [PATCH 02/23] update options and qlref files --- java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref | 2 +- java/ql/test/query-tests/security/CWE-730/options | 2 +- .../ql/test/query-tests/security/CWE-730/options-regexInjection | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) delete mode 100644 java/ql/test/query-tests/security/CWE-730/options-regexInjection diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref b/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref index 36e4f927f0a7..c94b15751304 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref @@ -1 +1 @@ -experimental/Security/CWE/CWE-730/RegexInjection.ql +Security/CWE/CWE-730/RegexInjection.ql diff --git a/java/ql/test/query-tests/security/CWE-730/options b/java/ql/test/query-tests/security/CWE-730/options index 2f7d22dc61ca..884cb21114c3 100644 --- a/java/ql/test/query-tests/security/CWE-730/options +++ b/java/ql/test/query-tests/security/CWE-730/options @@ -1 +1 @@ -// semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/guava-30.0 \ No newline at end of file +// semmle-extractor-options: --javac-args -cp ${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/guava-30.0:${testdir}/../../../stubs/servlet-api-2.4:${testdir}/../../../stubs/apache-commons-lang3-3.7 diff --git a/java/ql/test/query-tests/security/CWE-730/options-regexInjection b/java/ql/test/query-tests/security/CWE-730/options-regexInjection deleted file mode 100644 index 73f1b5a3c3eb..000000000000 --- a/java/ql/test/query-tests/security/CWE-730/options-regexInjection +++ /dev/null @@ -1 +0,0 @@ -// semmle-extractor-options: --javac-args -cp ${testdir}/../../../../stubs/servlet-api-2.4:${testdir}/../../../../stubs/apache-commons-lang3-3.7 \ No newline at end of file From 833c5edf067f01e9e4d3d62bc20c60811044b6a8 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Mon, 24 Oct 2022 14:29:06 -0400 Subject: [PATCH 03/23] move to .qll file and switch to InlineExpectations tests --- .../java/security/RegexInjectionQuery.qll | 75 ++++++++++++++++ .../Security/CWE/CWE-730/RegexInjection.ql | 85 +------------------ .../security/CWE-730/RegexInjection.expected | 73 ---------------- .../security/CWE-730/RegexInjection.qlref | 1 - .../CWE-730/RegexInjectionTest.expected | 0 ...Injection.java => RegexInjectionTest.java} | 34 ++++---- .../security/CWE-730/RegexInjectionTest.ql | 21 +++++ 7 files changed, 114 insertions(+), 175 deletions(-) create mode 100644 java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll delete mode 100644 java/ql/test/query-tests/security/CWE-730/RegexInjection.expected delete mode 100644 java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref create mode 100644 java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.expected rename java/ql/test/query-tests/security/CWE-730/{RegexInjection.java => RegexInjectionTest.java} (82%) create mode 100644 java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll new file mode 100644 index 000000000000..c91a49243ad4 --- /dev/null +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -0,0 +1,75 @@ +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking + +/** + * A data flow sink for untrusted user input used to construct regular expressions. + */ +class RegexSink extends DataFlow::ExprNode { + RegexSink() { + exists(MethodAccess ma, Method m | m = ma.getMethod() | + ( + m.getDeclaringType() instanceof TypeString and + ( + ma.getArgument(0) = this.asExpr() and + // TODO: confirm if more/less than the below need to be handled + m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) + ) + or + // TODO: review Java Pattern API + m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and + ( + ma.getArgument(0) = this.asExpr() and + // TODO: confirm if more/less than the below need to be handled + m.hasName(["compile", "matches"]) + ) + or + // TODO: read docs about regex APIs in Java + m.getDeclaringType().hasQualifiedName("org.apache.commons.lang3", "RegExUtils") and + ( + ma.getArgument(1) = this.asExpr() and + m.getParameterType(1) instanceof TypeString and + // TODO: confirm if more/less than the below need to be handled + m.hasName([ + "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", + "replacePattern" + ]) + ) + ) + ) + } +} + +// TODO: is this abstract class needed? Are there pre-existing sanitizer classes that can be used instead? +abstract class Sanitizer extends DataFlow::ExprNode { } + +/** + * A call to a function whose name suggests that it escapes regular + * expression meta-characters. + */ +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 // TODO: confirm this is sufficient + regexp = "regexp?" // TODO: confirm this is sufficient + | + calleeName + .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + + ".*)") // TODO: confirm this is sufficient + ) + } +} + +/** + * A taint-tracking configuration for untrusted user input used to construct regular expressions. + */ +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 } +} diff --git a/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql index 94bfccd9d3e2..5f56eefb25ad 100644 --- a/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql @@ -13,93 +13,10 @@ */ import java -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.security.RegexInjectionQuery import DataFlow::PathGraph -/** - * A data flow sink for untrusted user input used to construct regular expressions. - */ -class RegexSink extends DataFlow::ExprNode { - RegexSink() { - exists(MethodAccess ma, Method m | m = ma.getMethod() | - ( - m.getDeclaringType() instanceof TypeString and - ( - ma.getArgument(0) = this.asExpr() and - // TODO: confirm if more/less than the below need to be handled - m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) - ) - or - // TODO: review Java Pattern API - m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and - ( - ma.getArgument(0) = this.asExpr() and - // TODO: confirm if more/less than the below need to be handled - m.hasName(["compile", "matches"]) - ) - or - // TODO: read docs about regex APIs in Java - m.getDeclaringType().hasQualifiedName("org.apache.commons.lang3", "RegExUtils") and - ( - ma.getArgument(1) = this.asExpr() and - m.getParameterType(1) instanceof TypeString and - // TODO: confirm if more/less than the below need to be handled - m.hasName([ - "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", - "replacePattern" - ]) - ) - ) - ) - } -} - -// TODO: is this abstract class needed? Are there pre-existing sanitizer classes that can be used instead? -abstract class Sanitizer extends DataFlow::ExprNode { } - -/** - * A call to a function whose name suggests that it escapes regular - * expression meta-characters. - */ -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 // TODO: confirm this is sufficient - regexp = "regexp?" // TODO: confirm this is sufficient - | - calleeName - .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + - ".*)") // TODO: confirm this is sufficient - ) - } -} - -/** - * A taint-tracking configuration for untrusted user input used to construct regular expressions. - */ -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, "This regular expression is constructed from a $@.", source.getNode(), "user-provided value" -// from MethodAccess ma -// where -// // ma.getMethod().hasName("startsWith") and // graphhopper -// // ma.getFile().getBaseName() = "NavigateResource.java" // graphhopper -// // ma.getMethod().hasName("substring") and // jfinal -// // ma.getFile().getBaseName() = "FileManager.java" // jfinal -// ma.getMethod().hasName("startsWith") and // roller -// ma.getFile().getBaseName() = "PageServlet.java" // roller (or RegexUtil.java) -// ProteinArraySignificanceTestJSON.java or MockRKeys.java for cbioportal -// select ma, "method access" diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjection.expected b/java/ql/test/query-tests/security/CWE-730/RegexInjection.expected deleted file mode 100644 index a795f1591ada..000000000000 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjection.expected +++ /dev/null @@ -1,73 +0,0 @@ -edges -| RegexInjection.java:13:22:13:52 | getParameter(...) : String | RegexInjection.java:16:26:16:47 | ... + ... | -| RegexInjection.java:20:22:20:52 | getParameter(...) : String | RegexInjection.java:23:24:23:30 | pattern | -| RegexInjection.java:27:22:27:52 | getParameter(...) : String | RegexInjection.java:30:31:30:37 | pattern | -| RegexInjection.java:34:22:34:52 | getParameter(...) : String | RegexInjection.java:37:29:37:35 | pattern | -| RegexInjection.java:41:22:41:52 | getParameter(...) : String | RegexInjection.java:44:34:44:40 | pattern | -| RegexInjection.java:51:22:51:52 | getParameter(...) : String | RegexInjection.java:54:28:54:34 | pattern | -| RegexInjection.java:58:22:58:52 | getParameter(...) : String | RegexInjection.java:61:28:61:34 | pattern | -| RegexInjection.java:65:22:65:52 | getParameter(...) : String | RegexInjection.java:68:36:68:42 | pattern : String | -| RegexInjection.java:68:32:68:43 | foo(...) : String | RegexInjection.java:68:26:68:52 | ... + ... | -| RegexInjection.java:68:36:68:42 | pattern : String | RegexInjection.java:68:32:68:43 | foo(...) : String | -| RegexInjection.java:68:36:68:42 | pattern : String | RegexInjection.java:71:14:71:23 | str : String | -| RegexInjection.java:71:14:71:23 | str : String | RegexInjection.java:72:12:72:14 | str : String | -| RegexInjection.java:84:22:84:52 | getParameter(...) : String | RegexInjection.java:90:26:90:47 | ... + ... | -| RegexInjection.java:100:22:100:52 | getParameter(...) : String | RegexInjection.java:103:40:103:46 | pattern | -| RegexInjection.java:107:22:107:52 | getParameter(...) : String | RegexInjection.java:110:42:110:48 | pattern | -| RegexInjection.java:114:22:114:52 | getParameter(...) : String | RegexInjection.java:117:44:117:50 | pattern | -| RegexInjection.java:121:22:121:52 | getParameter(...) : String | RegexInjection.java:124:41:124:47 | pattern | -| RegexInjection.java:128:22:128:52 | getParameter(...) : String | RegexInjection.java:131:43:131:49 | pattern | -| RegexInjection.java:143:22:143:52 | getParameter(...) : String | RegexInjection.java:146:45:146:51 | pattern | -nodes -| RegexInjection.java:13:22:13:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:16:26:16:47 | ... + ... | semmle.label | ... + ... | -| RegexInjection.java:20:22:20:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:23:24:23:30 | pattern | semmle.label | pattern | -| RegexInjection.java:27:22:27:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:30:31:30:37 | pattern | semmle.label | pattern | -| RegexInjection.java:34:22:34:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:37:29:37:35 | pattern | semmle.label | pattern | -| RegexInjection.java:41:22:41:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:44:34:44:40 | pattern | semmle.label | pattern | -| RegexInjection.java:51:22:51:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:54:28:54:34 | pattern | semmle.label | pattern | -| RegexInjection.java:58:22:58:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:61:28:61:34 | pattern | semmle.label | pattern | -| RegexInjection.java:65:22:65:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:68:26:68:52 | ... + ... | semmle.label | ... + ... | -| RegexInjection.java:68:32:68:43 | foo(...) : String | semmle.label | foo(...) : String | -| RegexInjection.java:68:36:68:42 | pattern : String | semmle.label | pattern : String | -| RegexInjection.java:71:14:71:23 | str : String | semmle.label | str : String | -| RegexInjection.java:72:12:72:14 | str : String | semmle.label | str : String | -| RegexInjection.java:84:22:84:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:90:26:90:47 | ... + ... | semmle.label | ... + ... | -| RegexInjection.java:100:22:100:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:103:40:103:46 | pattern | semmle.label | pattern | -| RegexInjection.java:107:22:107:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:110:42:110:48 | pattern | semmle.label | pattern | -| RegexInjection.java:114:22:114:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:117:44:117:50 | pattern | semmle.label | pattern | -| RegexInjection.java:121:22:121:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:124:41:124:47 | pattern | semmle.label | pattern | -| RegexInjection.java:128:22:128:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:131:43:131:49 | pattern | semmle.label | pattern | -| RegexInjection.java:143:22:143:52 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| RegexInjection.java:146:45:146:51 | pattern | semmle.label | pattern | -subpaths -| RegexInjection.java:68:36:68:42 | pattern : String | RegexInjection.java:71:14:71:23 | str : String | RegexInjection.java:72:12:72:14 | str : String | RegexInjection.java:68:32:68:43 | foo(...) : String | -#select -| RegexInjection.java:16:26:16:47 | ... + ... | RegexInjection.java:13:22:13:52 | getParameter(...) : String | RegexInjection.java:16:26:16:47 | ... + ... | This regular expression is constructed from a $@. | RegexInjection.java:13:22:13:52 | getParameter(...) | user-provided value | -| RegexInjection.java:23:24:23:30 | pattern | RegexInjection.java:20:22:20:52 | getParameter(...) : String | RegexInjection.java:23:24:23:30 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:20:22:20:52 | getParameter(...) | user-provided value | -| RegexInjection.java:30:31:30:37 | pattern | RegexInjection.java:27:22:27:52 | getParameter(...) : String | RegexInjection.java:30:31:30:37 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:27:22:27:52 | getParameter(...) | user-provided value | -| RegexInjection.java:37:29:37:35 | pattern | RegexInjection.java:34:22:34:52 | getParameter(...) : String | RegexInjection.java:37:29:37:35 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:34:22:34:52 | getParameter(...) | user-provided value | -| RegexInjection.java:44:34:44:40 | pattern | RegexInjection.java:41:22:41:52 | getParameter(...) : String | RegexInjection.java:44:34:44:40 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:41:22:41:52 | getParameter(...) | user-provided value | -| RegexInjection.java:54:28:54:34 | pattern | RegexInjection.java:51:22:51:52 | getParameter(...) : String | RegexInjection.java:54:28:54:34 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:51:22:51:52 | getParameter(...) | user-provided value | -| RegexInjection.java:61:28:61:34 | pattern | RegexInjection.java:58:22:58:52 | getParameter(...) : String | RegexInjection.java:61:28:61:34 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:58:22:58:52 | getParameter(...) | user-provided value | -| RegexInjection.java:68:26:68:52 | ... + ... | RegexInjection.java:65:22:65:52 | getParameter(...) : String | RegexInjection.java:68:26:68:52 | ... + ... | This regular expression is constructed from a $@. | RegexInjection.java:65:22:65:52 | getParameter(...) | user-provided value | -| RegexInjection.java:90:26:90:47 | ... + ... | RegexInjection.java:84:22:84:52 | getParameter(...) : String | RegexInjection.java:90:26:90:47 | ... + ... | This regular expression is constructed from a $@. | RegexInjection.java:84:22:84:52 | getParameter(...) | user-provided value | -| RegexInjection.java:103:40:103:46 | pattern | RegexInjection.java:100:22:100:52 | getParameter(...) : String | RegexInjection.java:103:40:103:46 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:100:22:100:52 | getParameter(...) | user-provided value | -| RegexInjection.java:110:42:110:48 | pattern | RegexInjection.java:107:22:107:52 | getParameter(...) : String | RegexInjection.java:110:42:110:48 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:107:22:107:52 | getParameter(...) | user-provided value | -| RegexInjection.java:117:44:117:50 | pattern | RegexInjection.java:114:22:114:52 | getParameter(...) : String | RegexInjection.java:117:44:117:50 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:114:22:114:52 | getParameter(...) | user-provided value | -| RegexInjection.java:124:41:124:47 | pattern | RegexInjection.java:121:22:121:52 | getParameter(...) : String | RegexInjection.java:124:41:124:47 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:121:22:121:52 | getParameter(...) | user-provided value | -| RegexInjection.java:131:43:131:49 | pattern | RegexInjection.java:128:22:128:52 | getParameter(...) : String | RegexInjection.java:131:43:131:49 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:128:22:128:52 | getParameter(...) | user-provided value | -| RegexInjection.java:146:45:146:51 | pattern | RegexInjection.java:143:22:143:52 | getParameter(...) : String | RegexInjection.java:146:45:146:51 | pattern | This regular expression is constructed from a $@. | RegexInjection.java:143:22:143:52 | getParameter(...) | user-provided value | diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref b/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref deleted file mode 100644 index c94b15751304..000000000000 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjection.qlref +++ /dev/null @@ -1 +0,0 @@ -Security/CWE/CWE-730/RegexInjection.ql diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.expected b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjection.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java similarity index 82% rename from java/ql/test/query-tests/security/CWE-730/RegexInjection.java rename to java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index 2aace93aeb8c..fa86089515b2 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjection.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -8,64 +8,64 @@ import org.apache.commons.lang3.RegExUtils; -public class RegexInjection extends HttpServlet { +public class RegexInjectionTest 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 + return input.matches("^" + pattern + "=.*$"); // $ hasRegexInjection } 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 + return input.split(pattern).length > 0; // $ hasRegexInjection } 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 + return input.replaceFirst(pattern, "").length() > 0; // $ hasRegexInjection } 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 + return input.replaceAll(pattern, "").length() > 0; // $ hasRegexInjection } public boolean pattern1(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - Pattern pt = Pattern.compile(pattern); + Pattern pt = Pattern.compile(pattern); // $ hasRegexInjection Matcher matcher = pt.matcher(input); - return matcher.find(); // BAD + return matcher.find(); // BAD // ! double-check that line 44 is the correct location for this alert (seems like it based on the old .expected file) } 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 + return Pattern.compile(pattern).matcher(input).matches(); // $ hasRegexInjection } public boolean pattern3(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return Pattern.matches(pattern, input); // BAD + return Pattern.matches(pattern, input); // $ hasRegexInjection } public boolean pattern4(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return input.matches("^" + foo(pattern) + "=.*$"); // BAD + return input.matches("^" + foo(pattern) + "=.*$"); // $ hasRegexInjection } String foo(String str) { @@ -87,7 +87,7 @@ public boolean pattern6(javax.servlet.http.HttpServletRequest request) { escapeSpecialRegexChars(pattern); // BAD: the pattern is not really sanitized - return input.matches("^" + pattern + "=.*$"); + return input.matches("^" + pattern + "=.*$"); // $ hasRegexInjection } Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]"); @@ -100,35 +100,35 @@ public boolean apache1(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return RegExUtils.removeAll(input, pattern).length() > 0; // BAD + return RegExUtils.removeAll(input, pattern).length() > 0; // $ hasRegexInjection } public boolean apache2(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return RegExUtils.removeFirst(input, pattern).length() > 0; // BAD + return RegExUtils.removeFirst(input, pattern).length() > 0; // $ hasRegexInjection } public boolean apache3(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return RegExUtils.removePattern(input, pattern).length() > 0; // BAD + return RegExUtils.removePattern(input, pattern).length() > 0; // $ hasRegexInjection } public boolean apache4(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return RegExUtils.replaceAll(input, pattern, "").length() > 0; // BAD + return RegExUtils.replaceAll(input, pattern, "").length() > 0; // $ hasRegexInjection } public boolean apache5(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return RegExUtils.replaceFirst(input, pattern, "").length() > 0; // BAD + return RegExUtils.replaceFirst(input, pattern, "").length() > 0; // $ hasRegexInjection } public boolean apache6(javax.servlet.http.HttpServletRequest request) { @@ -143,6 +143,6 @@ public boolean apache7(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return RegExUtils.replacePattern(input, pattern, "").length() > 0; // BAD + return RegExUtils.replacePattern(input, pattern, "").length() > 0; // $ hasRegexInjection } } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql new file mode 100644 index 000000000000..596decf98af9 --- /dev/null +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql @@ -0,0 +1,21 @@ +import java +import TestUtilities.InlineExpectationsTest +import semmle.code.java.security.RegexInjectionQuery + +//import semmle.code.java.security.regexp.PolynomialReDoSQuery +class RegexInjectionTest extends InlineExpectationsTest { + RegexInjectionTest() { this = "RegexInjectionTest" } + + override string getARelevantTag() { result = "hasRegexInjection" } + + override predicate hasActualResult(Location location, string element, string tag, string value) { + tag = "hasRegexInjection" and + exists(DataFlow::PathNode source, DataFlow::PathNode sink, RegexInjectionConfiguration c | + c.hasFlowPath(source, sink) + | + location = sink.getNode().getLocation() and + element = sink.getNode().toString() and + value = "" + ) + } +} From 6545cff0efc0767f7360a9ebed014b21eb4ef5d6 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 25 Oct 2022 17:07:52 -0400 Subject: [PATCH 04/23] add Pattern.quote sanitizer --- .../java/security/RegexInjectionQuery.qll | 29 ++++++++++++++----- .../security/CWE-730/RegexInjectionTest.java | 18 ++++++++++-- 2 files changed, 37 insertions(+), 10 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index c91a49243ad4..a56d26b71831 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -11,25 +11,26 @@ class RegexSink extends DataFlow::ExprNode { ( m.getDeclaringType() instanceof TypeString and ( - ma.getArgument(0) = this.asExpr() and - // TODO: confirm if more/less than the below need to be handled + ma.getArgument(0) = this.asExpr() and // ! combine this line with the below at least? e.g. TypeString and TypePattern both use it + // ! test below more? + // ! (are there already classes for these methods in a regex library?) m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) ) or - // TODO: review Java Pattern API + // ! make class for the below? (is there already a class for this and its methods in a regex library?) m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and ( ma.getArgument(0) = this.asExpr() and - // TODO: confirm if more/less than the below need to be handled + // ! look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream m.hasName(["compile", "matches"]) ) or - // TODO: read docs about regex APIs in Java + // ! make class for the below? (is there already a class for this and its methods in a regex library?) m.getDeclaringType().hasQualifiedName("org.apache.commons.lang3", "RegExUtils") and ( ma.getArgument(1) = this.asExpr() and m.getParameterType(1) instanceof TypeString and - // TODO: confirm if more/less than the below need to be handled + // ! test below more? m.hasName([ "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", "replacePattern" @@ -40,17 +41,21 @@ class RegexSink extends DataFlow::ExprNode { } } -// TODO: is this abstract class needed? Are there pre-existing sanitizer classes that can be used instead? +// ! keep and rename to RegexInjectionSanitizer IF makes sense to have two sanitizers extending it?; +// ! else, ask Tony/others about if stylistically better to keep it (see default example in LogInjection.qll, etc.) +// ! maybe make abstract classes for source and sink as well (if you do this, mention it in PR description as an attempt to be similar to the other languages' implementations) abstract class Sanitizer extends DataFlow::ExprNode { } /** * A call to a function whose name suggests that it escapes regular * expression meta-characters. */ +// ! rename as DefaultRegexInjectionSanitizer? class RegExpSanitizationCall extends Sanitizer { RegExpSanitizationCall() { exists(string calleeName, string sanitize, string regexp | calleeName = this.asExpr().(Call).getCallee().getName() and + // ! add test case for sanitize? I think current tests only check escape sanitize = "(?:escape|saniti[sz]e)" and // TODO: confirm this is sufficient regexp = "regexp?" // TODO: confirm this is sufficient | @@ -58,6 +63,16 @@ class RegExpSanitizationCall extends Sanitizer { .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + ".*)") // TODO: confirm this is sufficient ) + or + // adds Pattern.quote() as a sanitizer + // see https://rules.sonarsource.com/java/RSPEC-2631 and https://sensei.securecodewarrior.com/recipes/scw:java:regex-injection + exists(MethodAccess ma, Method m | m = ma.getMethod() | + m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and + ( + ma.getArgument(0) = this.asExpr() and + m.hasName("quote") + ) + ) } } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index fa86089515b2..966893b053f1 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -44,7 +44,7 @@ public boolean pattern1(javax.servlet.http.HttpServletRequest request) { Pattern pt = Pattern.compile(pattern); // $ hasRegexInjection Matcher matcher = pt.matcher(input); - return matcher.find(); // BAD // ! double-check that line 44 is the correct location for this alert (seems like it based on the old .expected file) + return matcher.find(); } public boolean pattern2(javax.servlet.http.HttpServletRequest request) { @@ -76,7 +76,7 @@ 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 + // Safe: User input is sanitized before constructing the regex return input.matches("^" + escapeSpecialRegexChars(pattern) + "=.*$"); } @@ -84,6 +84,7 @@ public boolean pattern6(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); + // TODO: add a "Safe" test for situation when this return value is stored in a variable, then the variable is used in the regex escapeSpecialRegexChars(pattern); // BAD: the pattern is not really sanitized @@ -92,6 +93,7 @@ public boolean pattern6(javax.servlet.http.HttpServletRequest request) { Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]"); + // TODO: check if any other inbuilt escape/sanitizers in Java APIs String escapeSpecialRegexChars(String str) { return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); } @@ -136,7 +138,7 @@ public boolean apache6(javax.servlet.http.HttpServletRequest request) { String input = request.getParameter("input"); Pattern pt = (Pattern)(Object) pattern; - return RegExUtils.replaceFirst(input, pt, "").length() > 0; // GOOD, Pattern compile is the sink instead + return RegExUtils.replaceFirst(input, pt, "").length() > 0; // Safe: Pattern compile is the sink instead } public boolean apache7(javax.servlet.http.HttpServletRequest request) { @@ -145,4 +147,14 @@ public boolean apache7(javax.servlet.http.HttpServletRequest request) { return RegExUtils.replacePattern(input, pattern, "").length() > 0; // $ hasRegexInjection } + + // from https://rules.sonarsource.com/java/RSPEC-2631 to test for Pattern.quote as safe + public boolean validate(javax.servlet.http.HttpServletRequest request) { + String regex = request.getParameter("regex"); + String input = request.getParameter("input"); + + return input.matches(Pattern.quote(regex)); // Safe: with Pattern.quote, metacharacters or escape sequences will be given no special meaning + } } + +// ! see the following for potential additional test case ideas: https://www.baeldung.com/regular-expressions-java From 6ba7449df7d06341807301834c6fc052fc3d9aa5 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 27 Oct 2022 16:04:22 -0400 Subject: [PATCH 05/23] adjust imports From 037a05cd66d5d0be4dd689b5a4d22bc349a320bf Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 27 Oct 2022 16:38:12 -0400 Subject: [PATCH 06/23] add classes for Pattern, Matcher, and RegExUtils --- .../java/security/RegexInjectionQuery.qll | 58 ++++++++++--------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index a56d26b71831..00553d78cbd1 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -2,40 +2,46 @@ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking +/** The Java class `java.util.regex.Pattern`. */ +private class RegexPattern extends RefType { + RegexPattern() { this.hasQualifiedName("java.util.regex", "Pattern") } +} + +/** The Java class `java.util.regex.Matcher`. */ +private class RegexMatcher extends RefType { + RegexMatcher() { this.hasQualifiedName("java.util.regex", "Matcher") } +} + +/** The Java class `org.apache.commons.lang3.RegExUtils`. */ +private class ApacheRegExUtils extends RefType { + ApacheRegExUtils() { this.hasQualifiedName("java.util.regex", "Matcher") } +} + +// TODO: Are there already classes for any of below(above) in a pre-existing regex library? +// TODO: look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream /** * A data flow sink for untrusted user input used to construct regular expressions. */ class RegexSink extends DataFlow::ExprNode { RegexSink() { exists(MethodAccess ma, Method m | m = ma.getMethod() | + ma.getArgument(0) = this.asExpr() and ( m.getDeclaringType() instanceof TypeString and - ( - ma.getArgument(0) = this.asExpr() and // ! combine this line with the below at least? e.g. TypeString and TypePattern both use it - // ! test below more? - // ! (are there already classes for these methods in a regex library?) - m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) - ) - or - // ! make class for the below? (is there already a class for this and its methods in a regex library?) - m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and - ( - ma.getArgument(0) = this.asExpr() and - // ! look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream - m.hasName(["compile", "matches"]) - ) + m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) or - // ! make class for the below? (is there already a class for this and its methods in a regex library?) - m.getDeclaringType().hasQualifiedName("org.apache.commons.lang3", "RegExUtils") and - ( - ma.getArgument(1) = this.asExpr() and - m.getParameterType(1) instanceof TypeString and - // ! test below more? - m.hasName([ - "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", - "replacePattern" - ]) - ) + m.getDeclaringType() instanceof RegexPattern and + m.hasName(["compile", "matches"]) + ) + or + m.getDeclaringType() instanceof ApacheRegExUtils and + ( + ma.getArgument(1) = this.asExpr() and + m.getParameterType(1) instanceof TypeString and // only does String here because other option is Patter, but that's already handled by `java.util.regex.Pattern` above + m.hasName([ + "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", + "replacePattern" + ]) ) ) } @@ -67,7 +73,7 @@ class RegExpSanitizationCall extends Sanitizer { // adds Pattern.quote() as a sanitizer // see https://rules.sonarsource.com/java/RSPEC-2631 and https://sensei.securecodewarrior.com/recipes/scw:java:regex-injection exists(MethodAccess ma, Method m | m = ma.getMethod() | - m.getDeclaringType().hasQualifiedName("java.util.regex", "Pattern") and + m.getDeclaringType() instanceof RegexPattern and ( ma.getArgument(0) = this.asExpr() and m.hasName("quote") From f6f26fe6c5b4dba43982d1b938dc0b00b2c39162 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Mon, 31 Oct 2022 09:53:28 -0400 Subject: [PATCH 07/23] refactor code; add change note --- java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll | 3 ++- .../lib/semmle/code/java/security/RegexInjectionQuery.qll | 8 ++++++-- java/ql/src/Security/CWE/CWE-730/RegexInjection.ql | 3 ++- java/ql/src/change-notes/2022-10-28-regex-injection.md | 4 ++++ 4 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 java/ql/src/change-notes/2022-10-28-regex-injection.md diff --git a/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll b/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll index 8936de5a9239..08bf770b7640 100644 --- a/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll +++ b/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll @@ -39,7 +39,8 @@ private predicate regexSinkKindInfo(string kind, boolean full, int strArg) { } /** A sink that is relevant for regex flow. */ -private class RegexFlowSink extends DataFlow::Node { +class RegexFlowSink extends DataFlow::Node { + // ! switch back to private!!! - just testing if this sink is useful for regex injection as well boolean full; int strArg; diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index 00553d78cbd1..b28a6e7d4d6d 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -1,6 +1,7 @@ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.regex.RegexFlowConfigs /** The Java class `java.util.regex.Pattern`. */ private class RegexPattern extends RefType { @@ -17,7 +18,7 @@ private class ApacheRegExUtils extends RefType { ApacheRegExUtils() { this.hasQualifiedName("java.util.regex", "Matcher") } } -// TODO: Are there already classes for any of below(above) in a pre-existing regex library? +// TODO: Look for above in pre-existing regex libraries again. // TODO: look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream /** * A data flow sink for untrusted user input used to construct regular expressions. @@ -37,7 +38,7 @@ class RegexSink extends DataFlow::ExprNode { m.getDeclaringType() instanceof ApacheRegExUtils and ( ma.getArgument(1) = this.asExpr() and - m.getParameterType(1) instanceof TypeString and // only does String here because other option is Patter, but that's already handled by `java.util.regex.Pattern` above + m.getParameterType(1) instanceof TypeString and // only does String here because other option is Pattern, but that's already handled by `java.util.regex.Pattern` above m.hasName([ "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", "replacePattern" @@ -92,5 +93,8 @@ class RegexInjectionConfiguration extends TaintTracking::Configuration { override predicate isSink(DataFlow::Node sink) { sink instanceof RegexSink } + // ! testing below RegexFlowSink from RegexFlowConfigs.qll + // ! extra results from jfinal with this... look into further... + // override predicate isSink(DataFlow::Node sink) { sink instanceof RegexFlowSink } override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer } } diff --git a/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql index 5f56eefb25ad..0f0175b30cd8 100644 --- a/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql @@ -1,10 +1,11 @@ /** * @name Regular expression injection - * @description User input should not be used in regular expressions without first being sanitized, + * @description User input should not be used in regular expressions without first being escaped, * 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 + * @security-severity 7.5 * @precision high * @id java/regex-injection * @tags security diff --git a/java/ql/src/change-notes/2022-10-28-regex-injection.md b/java/ql/src/change-notes/2022-10-28-regex-injection.md new file mode 100644 index 000000000000..f2376f34c8a0 --- /dev/null +++ b/java/ql/src/change-notes/2022-10-28-regex-injection.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* Added a new query, `java/regex-injection`, to detect unescaped user input used in regular expressions. From 50d638d1b6c9b055a15fe0e8bd24d84c732905cd Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Mon, 31 Oct 2022 15:08:29 -0400 Subject: [PATCH 08/23] create RegexInjection.qll file --- .../code/java/regex/RegexFlowConfigs.qll | 3 +- .../code/java/security/RegexInjection.qll | 93 +++++++++++++++++++ .../java/security/RegexInjectionQuery.qll | 91 +----------------- 3 files changed, 99 insertions(+), 88 deletions(-) create mode 100644 java/ql/lib/semmle/code/java/security/RegexInjection.qll diff --git a/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll b/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll index 08bf770b7640..8936de5a9239 100644 --- a/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll +++ b/java/ql/lib/semmle/code/java/regex/RegexFlowConfigs.qll @@ -39,8 +39,7 @@ private predicate regexSinkKindInfo(string kind, boolean full, int strArg) { } /** A sink that is relevant for regex flow. */ -class RegexFlowSink extends DataFlow::Node { - // ! switch back to private!!! - just testing if this sink is useful for regex injection as well +private class RegexFlowSink extends DataFlow::Node { boolean full; int strArg; diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll new file mode 100644 index 000000000000..614f3ff67bc0 --- /dev/null +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -0,0 +1,93 @@ +/** Provides classes and predicates related to regex injection in Java. */ + +import java +import semmle.code.java.dataflow.FlowSources +import semmle.code.java.dataflow.TaintTracking +import semmle.code.java.regex.RegexFlowConfigs + +/** + * A data flow sink for untrusted user input used to construct regular expressions. + */ +abstract class Sink extends DataFlow::ExprNode { } + +/** + * A sanitizer for untrusted user input used to construct regular expressions. + */ +abstract class Sanitizer extends DataFlow::ExprNode { } + +// TODO: look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream +/** + * A data flow sink for untrusted user input used to construct regular expressions. + */ +private class RegexSink extends Sink { + RegexSink() { + exists(MethodAccess ma, Method m | m = ma.getMethod() | + ma.getArgument(0) = this.asExpr() and + ( + m.getDeclaringType() instanceof TypeString and + m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) + or + m.getDeclaringType() instanceof RegexPattern and + m.hasName(["compile", "matches"]) + ) + or + m.getDeclaringType() instanceof ApacheRegExUtils and + ( + ma.getArgument(1) = this.asExpr() and + // only handles String param here because the other param option, Pattern, is already handled by `java.util.regex.Pattern` above + m.getParameterType(1) instanceof TypeString and + m.hasName([ + "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", + "replacePattern" + ]) + ) + ) + } +} + +/** + * A call to a function whose name suggests that it escapes regular + * expression meta-characters. + */ +class RegexInjectionSanitizer extends Sanitizer { + RegexInjectionSanitizer() { + exists(string calleeName, string sanitize, string regexp | + calleeName = this.asExpr().(Call).getCallee().getName() and + // TODO: add test case for sanitize? I think current tests only check escape + // TODO: should this be broader and only look for "escape|saniti[sz]e" and not "regexp?" as well? -- e.g. err on side of FNs? + sanitize = "(?:escape|saniti[sz]e)" and + regexp = "regexp?" + | + calleeName + .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + + ".*)") + ) + or + // adds Pattern.quote() as a sanitizer + // https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#quote-java.lang.String-: "Metacharacters or escape sequences in the input sequence will be given no special meaning." + // see https://rules.sonarsource.com/java/RSPEC-2631 and https://sensei.securecodewarrior.com/recipes/scw:java:regex-injection + exists(MethodAccess ma, Method m | m = ma.getMethod() | + m.getDeclaringType() instanceof RegexPattern and + ( + ma.getArgument(0) = this.asExpr() and + m.hasName("quote") + ) + ) + } +} + +// ******** HELPER CLASSES/METHODS (MAYBE MOVE ELSEWHERE?) ******** +// TODO: move below to Regex.qll?? +/** The Java class `java.util.regex.Pattern`. */ +private class RegexPattern extends RefType { + RegexPattern() { this.hasQualifiedName("java.util.regex", "Pattern") } +} + +// /** The Java class `java.util.regex.Matcher`. */ +// private class RegexMatcher extends RefType { +// RegexMatcher() { this.hasQualifiedName("java.util.regex", "Matcher") } +// } +/** The Java class `org.apache.commons.lang3.RegExUtils`. */ +private class ApacheRegExUtils extends RefType { + ApacheRegExUtils() { this.hasQualifiedName("org.apache.commons.lang3", "RegExUtils") } +} diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index b28a6e7d4d6d..e30ea091f3e0 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -1,100 +1,19 @@ +/** Provides configurations to be used in queries related to regex injection. */ + import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking -import semmle.code.java.regex.RegexFlowConfigs - -/** The Java class `java.util.regex.Pattern`. */ -private class RegexPattern extends RefType { - RegexPattern() { this.hasQualifiedName("java.util.regex", "Pattern") } -} - -/** The Java class `java.util.regex.Matcher`. */ -private class RegexMatcher extends RefType { - RegexMatcher() { this.hasQualifiedName("java.util.regex", "Matcher") } -} - -/** The Java class `org.apache.commons.lang3.RegExUtils`. */ -private class ApacheRegExUtils extends RefType { - ApacheRegExUtils() { this.hasQualifiedName("java.util.regex", "Matcher") } -} - -// TODO: Look for above in pre-existing regex libraries again. -// TODO: look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream -/** - * A data flow sink for untrusted user input used to construct regular expressions. - */ -class RegexSink extends DataFlow::ExprNode { - RegexSink() { - exists(MethodAccess ma, Method m | m = ma.getMethod() | - ma.getArgument(0) = this.asExpr() and - ( - m.getDeclaringType() instanceof TypeString and - m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) - or - m.getDeclaringType() instanceof RegexPattern and - m.hasName(["compile", "matches"]) - ) - or - m.getDeclaringType() instanceof ApacheRegExUtils and - ( - ma.getArgument(1) = this.asExpr() and - m.getParameterType(1) instanceof TypeString and // only does String here because other option is Pattern, but that's already handled by `java.util.regex.Pattern` above - m.hasName([ - "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", - "replacePattern" - ]) - ) - ) - } -} - -// ! keep and rename to RegexInjectionSanitizer IF makes sense to have two sanitizers extending it?; -// ! else, ask Tony/others about if stylistically better to keep it (see default example in LogInjection.qll, etc.) -// ! maybe make abstract classes for source and sink as well (if you do this, mention it in PR description as an attempt to be similar to the other languages' implementations) -abstract class Sanitizer extends DataFlow::ExprNode { } - -/** - * A call to a function whose name suggests that it escapes regular - * expression meta-characters. - */ -// ! rename as DefaultRegexInjectionSanitizer? -class RegExpSanitizationCall extends Sanitizer { - RegExpSanitizationCall() { - exists(string calleeName, string sanitize, string regexp | - calleeName = this.asExpr().(Call).getCallee().getName() and - // ! add test case for sanitize? I think current tests only check escape - sanitize = "(?:escape|saniti[sz]e)" and // TODO: confirm this is sufficient - regexp = "regexp?" // TODO: confirm this is sufficient - | - calleeName - .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + - ".*)") // TODO: confirm this is sufficient - ) - or - // adds Pattern.quote() as a sanitizer - // see https://rules.sonarsource.com/java/RSPEC-2631 and https://sensei.securecodewarrior.com/recipes/scw:java:regex-injection - exists(MethodAccess ma, Method m | m = ma.getMethod() | - m.getDeclaringType() instanceof RegexPattern and - ( - ma.getArgument(0) = this.asExpr() and - m.hasName("quote") - ) - ) - } -} +import semmle.code.java.security.RegexInjection /** * A taint-tracking configuration for untrusted user input used to construct regular expressions. */ class RegexInjectionConfiguration extends TaintTracking::Configuration { - RegexInjectionConfiguration() { this = "RegexInjectionConfiguration" } + RegexInjectionConfiguration() { this = "RegexInjection" } override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { sink instanceof RegexSink } + override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } - // ! testing below RegexFlowSink from RegexFlowConfigs.qll - // ! extra results from jfinal with this... look into further... - // override predicate isSink(DataFlow::Node sink) { sink instanceof RegexFlowSink } override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer } } From 91491d9a7b0929492951a99f0c84515b199d42a0 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Mon, 31 Oct 2022 18:14:39 -0400 Subject: [PATCH 09/23] refactor into more classes; add more test cases; add LITERAL sanitizer --- .../lib/semmle/code/java/frameworks/Regex.qll | 5 + .../code/java/frameworks/apache/Lang.qll | 5 + .../code/java/security/RegexInjection.qll | 130 ++++++++++-------- .../java/security/RegexInjectionQuery.qll | 4 +- .../security/CWE-730/RegexInjectionTest.java | 53 ++++++- 5 files changed, 132 insertions(+), 65 deletions(-) diff --git a/java/ql/lib/semmle/code/java/frameworks/Regex.qll b/java/ql/lib/semmle/code/java/frameworks/Regex.qll index 4e83981d8574..4915cfbeeed5 100644 --- a/java/ql/lib/semmle/code/java/frameworks/Regex.qll +++ b/java/ql/lib/semmle/code/java/frameworks/Regex.qll @@ -2,6 +2,11 @@ private import semmle.code.java.dataflow.ExternalFlow +/** The class `java.util.regex.Pattern`. */ +class TypeRegexPattern extends Class { + TypeRegexPattern() { this.hasQualifiedName("java.util.regex", "Pattern") } +} + private class RegexModel extends SummaryModelCsv { override predicate row(string s) { s = diff --git a/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll b/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll index 84db672e9355..d2c94bcb376d 100644 --- a/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll +++ b/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll @@ -46,3 +46,8 @@ class TypeApacheSystemUtils extends Class { this.hasQualifiedName(["org.apache.commons.lang", "org.apache.commons.lang3"], "SystemUtils") } } + +/** The class `org.apache.commons.lang3.RegExUtils`. */ +class TypeApacheRegExUtils extends Class { + TypeApacheRegExUtils() { this.hasQualifiedName("org.apache.commons.lang3", "RegExUtils") } +} diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index 614f3ff67bc0..b2b64587fcdc 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -1,60 +1,37 @@ /** Provides classes and predicates related to regex injection in Java. */ import java -import semmle.code.java.dataflow.FlowSources -import semmle.code.java.dataflow.TaintTracking -import semmle.code.java.regex.RegexFlowConfigs +private import semmle.code.java.dataflow.DataFlow +private import semmle.code.java.frameworks.Regex +private import semmle.code.java.frameworks.apache.Lang -/** - * A data flow sink for untrusted user input used to construct regular expressions. - */ +/** A data flow sink for untrusted user input used to construct regular expressions. */ abstract class Sink extends DataFlow::ExprNode { } -/** - * A sanitizer for untrusted user input used to construct regular expressions. - */ +/** A sanitizer for untrusted user input used to construct regular expressions. */ abstract class Sanitizer extends DataFlow::ExprNode { } -// TODO: look into further: Pattern.matcher, .pattern() and .toString() as taint steps, .split and .splitAsStream -/** - * A data flow sink for untrusted user input used to construct regular expressions. - */ -private class RegexSink extends Sink { - RegexSink() { +private class RegexInjectionSink extends Sink { + RegexInjectionSink() { exists(MethodAccess ma, Method m | m = ma.getMethod() | ma.getArgument(0) = this.asExpr() and ( - m.getDeclaringType() instanceof TypeString and - m.hasName(["matches", "split", "replaceFirst", "replaceAll"]) - or - m.getDeclaringType() instanceof RegexPattern and - m.hasName(["compile", "matches"]) + m instanceof StringRegexMethod or + m instanceof PatternRegexMethod ) or - m.getDeclaringType() instanceof ApacheRegExUtils and - ( - ma.getArgument(1) = this.asExpr() and - // only handles String param here because the other param option, Pattern, is already handled by `java.util.regex.Pattern` above - m.getParameterType(1) instanceof TypeString and - m.hasName([ - "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", - "replacePattern" - ]) - ) + ma.getArgument(1) = this.asExpr() and + m instanceof ApacheRegExUtilsMethod ) } } -/** - * A call to a function whose name suggests that it escapes regular - * expression meta-characters. - */ -class RegexInjectionSanitizer extends Sanitizer { +/** A call to a function which escapes regular expression meta-characters. */ +private class RegexInjectionSanitizer extends Sanitizer { RegexInjectionSanitizer() { + // a function whose name suggests that it escapes regular expression meta-characters exists(string calleeName, string sanitize, string regexp | calleeName = this.asExpr().(Call).getCallee().getName() and - // TODO: add test case for sanitize? I think current tests only check escape - // TODO: should this be broader and only look for "escape|saniti[sz]e" and not "regexp?" as well? -- e.g. err on side of FNs? sanitize = "(?:escape|saniti[sz]e)" and regexp = "regexp?" | @@ -63,31 +40,70 @@ class RegexInjectionSanitizer extends Sanitizer { ".*)") ) or - // adds Pattern.quote() as a sanitizer - // https://docs.oracle.com/javase/8/docs/api/java/util/regex/Pattern.html#quote-java.lang.String-: "Metacharacters or escape sequences in the input sequence will be given no special meaning." - // see https://rules.sonarsource.com/java/RSPEC-2631 and https://sensei.securecodewarrior.com/recipes/scw:java:regex-injection + // a call to the `Pattern.quote` method, which gives metacharacters or escape sequences no special meaning exists(MethodAccess ma, Method m | m = ma.getMethod() | - m.getDeclaringType() instanceof RegexPattern and - ( - ma.getArgument(0) = this.asExpr() and - m.hasName("quote") - ) + ma.getArgument(0) = this.asExpr() and + m instanceof PatternQuoteMethod + ) + or + // use of Pattern.LITERAL flag with `Pattern.compile` which gives metacharacters or escape sequences no special meaning + exists(MethodAccess ma, Method m, Field field | m = ma.getMethod() | + ma.getArgument(0) = this.asExpr() and + m instanceof PatternRegexMethod and + m.hasName("compile") and + //ma.getArgument(1).toString() = "Pattern.LITERAL" and + field instanceof PatternLiteral and + ma.getArgument(1) = field.getAnAccess() ) } } -// ******** HELPER CLASSES/METHODS (MAYBE MOVE ELSEWHERE?) ******** -// TODO: move below to Regex.qll?? -/** The Java class `java.util.regex.Pattern`. */ -private class RegexPattern extends RefType { - RegexPattern() { this.hasQualifiedName("java.util.regex", "Pattern") } +/** + * The methods of the class `java.lang.String` that take a regular expression + * as a parameter. + */ +private class StringRegexMethod extends Method { + StringRegexMethod() { + this.getDeclaringType() instanceof TypeString and + this.hasName(["matches", "split", "replaceFirst", "replaceAll"]) + } +} + +/** + * The methods of the class `java.util.regex.Pattern` that take a regular + * expression as a parameter. + */ +private class PatternRegexMethod extends Method { + PatternRegexMethod() { + this.getDeclaringType() instanceof TypeRegexPattern and + this.hasName(["compile", "matches"]) + } } -// /** The Java class `java.util.regex.Matcher`. */ -// private class RegexMatcher extends RefType { -// RegexMatcher() { this.hasQualifiedName("java.util.regex", "Matcher") } -// } -/** The Java class `org.apache.commons.lang3.RegExUtils`. */ -private class ApacheRegExUtils extends RefType { - ApacheRegExUtils() { this.hasQualifiedName("org.apache.commons.lang3", "RegExUtils") } +/** The `quote` method of the `java.util.regex.Pattern` class. */ +private class PatternQuoteMethod extends Method { + PatternQuoteMethod() { this.hasName(["quote"]) } +} + +/** The `LITERAL` field of the `java.util.regex.Pattern` class. */ +private class PatternLiteral extends Field { + PatternLiteral() { + this.getDeclaringType() instanceof TypeRegexPattern and + this.hasName("LITERAL") + } +} + +/** + * The methods of the class `org.apache.commons.lang3.RegExUtils` that take + * a regular expression of type `String` as a parameter. + */ +private class ApacheRegExUtilsMethod extends Method { + ApacheRegExUtilsMethod() { + this.getDeclaringType() instanceof TypeApacheRegExUtils and + // only handles String param here because the other param option, Pattern, is already handled by `java.util.regex.Pattern` + this.getParameterType(1) instanceof TypeString and + this.hasName([ + "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", "replacePattern" + ]) + } } diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index e30ea091f3e0..ce4cab80bb45 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -5,9 +5,7 @@ import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking import semmle.code.java.security.RegexInjection -/** - * A taint-tracking configuration for untrusted user input used to construct regular expressions. - */ +/** A taint-tracking configuration for untrusted user input used to construct regular expressions. */ class RegexInjectionConfiguration extends TaintTracking::Configuration { RegexInjectionConfiguration() { this = "RegexInjection" } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index 966893b053f1..d914272278dc 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -84,20 +84,55 @@ public boolean pattern6(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - // TODO: add a "Safe" test for situation when this return value is stored in a variable, then the variable is used in the regex escapeSpecialRegexChars(pattern); // BAD: the pattern is not really sanitized return input.matches("^" + pattern + "=.*$"); // $ hasRegexInjection } + public boolean pattern7(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + String input = request.getParameter("input"); + + String escapedPattern = escapeSpecialRegexChars(pattern); + + // Safe: User input is sanitized before constructing the regex + return input.matches("^" + escapedPattern + "=.*$"); + } + + public boolean pattern8(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + String input = request.getParameter("input"); + + // Safe: User input is sanitized before constructing the regex + return input.matches("^" + sanitizeSpecialRegexChars(pattern) + "=.*$"); + } + + public boolean pattern9(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + String input = request.getParameter("input"); + + // Safe: User input is sanitized before constructing the regex + return input.matches("^" + sanitiseSpecialRegexChars(pattern) + "=.*$"); + } + Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]"); - // TODO: check if any other inbuilt escape/sanitizers in Java APIs + // test `escape...regex` String escapeSpecialRegexChars(String str) { return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); } + // test `sanitize...regex` + String sanitizeSpecialRegexChars(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); + } + + // test `sanitise...regex` + String sanitiseSpecialRegexChars(String str) { + return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); + } + public boolean apache1(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); @@ -148,12 +183,20 @@ public boolean apache7(javax.servlet.http.HttpServletRequest request) { return RegExUtils.replacePattern(input, pattern, "").length() > 0; // $ hasRegexInjection } - // from https://rules.sonarsource.com/java/RSPEC-2631 to test for Pattern.quote as safe - public boolean validate(javax.servlet.http.HttpServletRequest request) { + // test `Pattern.quote` as safe + public boolean quoteTest(javax.servlet.http.HttpServletRequest request) { String regex = request.getParameter("regex"); String input = request.getParameter("input"); - return input.matches(Pattern.quote(regex)); // Safe: with Pattern.quote, metacharacters or escape sequences will be given no special meaning + return input.matches(Pattern.quote(regex)); // Safe + } + + // test `Pattern.LITERAL` as safe + public boolean literalTest(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + String input = request.getParameter("input"); + + return Pattern.compile(pattern, Pattern.LITERAL).matcher(input).matches(); // Safe } } From 5b089bbb9c3143d04e0d862d2b0b4bf1eed8afa4 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 1 Nov 2022 11:02:36 -0400 Subject: [PATCH 10/23] split sanitizer into three --- .../code/java/security/RegexInjection.qll | 32 +++++++++++++------ .../java/security/RegexInjectionQuery.qll | 2 +- .../security/CWE-730/RegexInjectionTest.java | 4 +-- 3 files changed, 25 insertions(+), 13 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index b2b64587fcdc..9430a55d0a56 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -9,7 +9,7 @@ private import semmle.code.java.frameworks.apache.Lang abstract class Sink extends DataFlow::ExprNode { } /** A sanitizer for untrusted user input used to construct regular expressions. */ -abstract class Sanitizer extends DataFlow::ExprNode { } +abstract class RegexInjectionSanitizer extends DataFlow::ExprNode { } private class RegexInjectionSink extends Sink { RegexInjectionSink() { @@ -26,10 +26,9 @@ private class RegexInjectionSink extends Sink { } } -/** A call to a function which escapes regular expression meta-characters. */ -private class RegexInjectionSanitizer extends Sanitizer { - RegexInjectionSanitizer() { - // a function whose name suggests that it escapes regular expression meta-characters +/** A call to a function whose name suggests that it escapes regular expression meta-characters. */ +private class RegexSanitizationCall extends RegexInjectionSanitizer { + RegexSanitizationCall() { exists(string calleeName, string sanitize, string regexp | calleeName = this.asExpr().(Call).getCallee().getName() and sanitize = "(?:escape|saniti[sz]e)" and @@ -39,19 +38,32 @@ private class RegexInjectionSanitizer extends Sanitizer { .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + ".*)") ) - or - // a call to the `Pattern.quote` method, which gives metacharacters or escape sequences no special meaning + } +} + +/** + * A call to the `Pattern.quote` method, which gives metacharacters or escape sequences + * no special meaning. + */ +private class PatternQuoteCall extends RegexInjectionSanitizer { + PatternQuoteCall() { exists(MethodAccess ma, Method m | m = ma.getMethod() | ma.getArgument(0) = this.asExpr() and m instanceof PatternQuoteMethod ) - or - // use of Pattern.LITERAL flag with `Pattern.compile` which gives metacharacters or escape sequences no special meaning + } +} + +/** + * Use of the `Pattern.LITERAL` flag with `Pattern.compile`, which gives metacharacters + * or escape sequences no special meaning. + */ +private class PatternLiteralFlag extends RegexInjectionSanitizer { + PatternLiteralFlag() { exists(MethodAccess ma, Method m, Field field | m = ma.getMethod() | ma.getArgument(0) = this.asExpr() and m instanceof PatternRegexMethod and m.hasName("compile") and - //ma.getArgument(1).toString() = "Pattern.LITERAL" and field instanceof PatternLiteral and ma.getArgument(1) = field.getAnAccess() ) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index ce4cab80bb45..56337dc7d7b3 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -13,5 +13,5 @@ class RegexInjectionConfiguration extends TaintTracking::Configuration { override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } - override predicate isSanitizer(DataFlow::Node node) { node instanceof Sanitizer } + override predicate isSanitizer(DataFlow::Node node) { node instanceof RegexInjectionSanitizer } } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index d914272278dc..e9d6a1d7f3e8 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -183,7 +183,7 @@ public boolean apache7(javax.servlet.http.HttpServletRequest request) { return RegExUtils.replacePattern(input, pattern, "").length() > 0; // $ hasRegexInjection } - // test `Pattern.quote` as safe + // test `Pattern.quote` sanitizer public boolean quoteTest(javax.servlet.http.HttpServletRequest request) { String regex = request.getParameter("regex"); String input = request.getParameter("input"); @@ -191,7 +191,7 @@ public boolean quoteTest(javax.servlet.http.HttpServletRequest request) { return input.matches(Pattern.quote(regex)); // Safe } - // test `Pattern.LITERAL` as safe + // test `Pattern.LITERAL` sanitizer public boolean literalTest(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); From 81ad10bab5bbc9c856e6fd22d2263436269f9b57 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 1 Nov 2022 11:07:28 -0400 Subject: [PATCH 11/23] update sink names --- java/ql/lib/semmle/code/java/security/RegexInjection.qll | 6 +++--- .../lib/semmle/code/java/security/RegexInjectionQuery.qll | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index 9430a55d0a56..b5bf94e0f8d7 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -6,13 +6,13 @@ private import semmle.code.java.frameworks.Regex private import semmle.code.java.frameworks.apache.Lang /** A data flow sink for untrusted user input used to construct regular expressions. */ -abstract class Sink extends DataFlow::ExprNode { } +abstract class RegexInjectionSink extends DataFlow::ExprNode { } /** A sanitizer for untrusted user input used to construct regular expressions. */ abstract class RegexInjectionSanitizer extends DataFlow::ExprNode { } -private class RegexInjectionSink extends Sink { - RegexInjectionSink() { +private class DefaultRegexInjectionSink extends RegexInjectionSink { + DefaultRegexInjectionSink() { exists(MethodAccess ma, Method m | m = ma.getMethod() | ma.getArgument(0) = this.asExpr() and ( diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll index 56337dc7d7b3..914b3222d9f7 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll @@ -11,7 +11,7 @@ class RegexInjectionConfiguration extends TaintTracking::Configuration { override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } - override predicate isSink(DataFlow::Node sink) { sink instanceof Sink } + override predicate isSink(DataFlow::Node sink) { sink instanceof RegexInjectionSink } override predicate isSanitizer(DataFlow::Node node) { node instanceof RegexInjectionSanitizer } } From eb30e8fe9e798d87e1dec7ff72abf4bded331086 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 1 Nov 2022 11:23:55 -0400 Subject: [PATCH 12/23] move Pattern.quote and Pattern.LITERAL models to Regex.qll --- .../ql/lib/semmle/code/java/frameworks/Regex.qll | 16 ++++++++++++++++ .../semmle/code/java/security/RegexInjection.qll | 13 ------------- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/java/ql/lib/semmle/code/java/frameworks/Regex.qll b/java/ql/lib/semmle/code/java/frameworks/Regex.qll index 4915cfbeeed5..fc157634b9b6 100644 --- a/java/ql/lib/semmle/code/java/frameworks/Regex.qll +++ b/java/ql/lib/semmle/code/java/frameworks/Regex.qll @@ -7,6 +7,22 @@ class TypeRegexPattern extends Class { TypeRegexPattern() { this.hasQualifiedName("java.util.regex", "Pattern") } } +/** The `quote` method of the `java.util.regex.Pattern` class. */ +class PatternQuoteMethod extends Method { + PatternQuoteMethod() { + this.getDeclaringType() instanceof TypeRegexPattern and + this.hasName(["quote"]) + } +} + +/** The `LITERAL` field of the `java.util.regex.Pattern` class. */ +class PatternLiteral extends Field { + PatternLiteral() { + this.getDeclaringType() instanceof TypeRegexPattern and + this.hasName("LITERAL") + } +} + private class RegexModel extends SummaryModelCsv { override predicate row(string s) { s = diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index b5bf94e0f8d7..0a4bb1656be1 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -92,19 +92,6 @@ private class PatternRegexMethod extends Method { } } -/** The `quote` method of the `java.util.regex.Pattern` class. */ -private class PatternQuoteMethod extends Method { - PatternQuoteMethod() { this.hasName(["quote"]) } -} - -/** The `LITERAL` field of the `java.util.regex.Pattern` class. */ -private class PatternLiteral extends Field { - PatternLiteral() { - this.getDeclaringType() instanceof TypeRegexPattern and - this.hasName("LITERAL") - } -} - /** * The methods of the class `org.apache.commons.lang3.RegExUtils` that take * a regular expression of type `String` as a parameter. From 32f7348d30b049fd11c1fb02bfd1c0a4a44db839 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 1 Nov 2022 14:50:58 -0400 Subject: [PATCH 13/23] update help file --- .../code/java/security/RegexInjection.qll | 1 + .../Security/CWE/CWE-730/RegexInjection.java | 46 ++++++------------- .../Security/CWE/CWE-730/RegexInjection.qhelp | 11 +++-- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index 0a4bb1656be1..24520bab2302 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -11,6 +11,7 @@ abstract class RegexInjectionSink extends DataFlow::ExprNode { } /** A sanitizer for untrusted user input used to construct regular expressions. */ abstract class RegexInjectionSanitizer extends DataFlow::ExprNode { } +/** A method call that takes a regular expression as an argument. */ private class DefaultRegexInjectionSink extends RegexInjectionSink { DefaultRegexInjectionSink() { exists(MethodAccess ma, Method m | m = ma.getMethod() | diff --git a/java/ql/src/Security/CWE/CWE-730/RegexInjection.java b/java/ql/src/Security/CWE/CWE-730/RegexInjection.java index 30b74df0a0ff..673d5a2fa409 100644 --- a/java/ql/src/Security/CWE/CWE-730/RegexInjection.java +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.java @@ -1,38 +1,22 @@ -package com.example.demo; - -import java.util.regex.Matcher; import java.util.regex.Pattern; +import javax.servlet.http.HttpServlet; +import javax.servlet.http.HttpServletRequest; -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!"; - } +public class RegexInjectionDemo extends HttpServlet { - @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!"; + public boolean badExample(javax.servlet.http.HttpServletRequest request) { + String regex = request.getParameter("regex"); + String input = request.getParameter("input"); - return "doesn't match!"; - } + // BAD: Unsanitized user input is used to construct a regular expression + return input.matches(regex); + } - Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]"); + public boolean goodExample(javax.servlet.http.HttpServletRequest request) { + String regex = request.getParameter("regex"); + String input = request.getParameter("input"); - String escapeSpecialRegexChars(String str) { - return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); - } + // GOOD: User input is sanitized before constructing the regex + return input.matches(Pattern.quote(regex)); + } } diff --git a/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp b/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp index 6cd80b52f755..fc8ab33ca80a 100644 --- a/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp @@ -15,25 +15,25 @@ perform a Denial of Service attack.

Before embedding user input into a regular expression, use a sanitization function -to escape meta-characters that have special meaning. +such as Pattern.quote to escape meta-characters that have special meaning.

-The following example shows a HTTP request parameter that is used to construct a regular expression: +The following example shows an HTTP request parameter that is used to construct a regular expression.

-

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.

-In the second case, the user input is escaped using escapeSpecialRegexChars before being included +In the second case, the user input is escaped using Pattern.quote before being included in the regular expression. This ensures that the user cannot insert characters which have a special meaning in regular expressions.

+
@@ -44,5 +44,8 @@ OWASP:
  • Wikipedia: ReDoS.
  • +
  • +Java API Specification: Pattern.quote. +
  • From 5dcd3b2c0fef6692f2484ec9ec192fbccf259134 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 1 Nov 2022 15:21:23 -0400 Subject: [PATCH 14/23] clean up files --- .../lib/semmle/code/java/security/RegexInjection.qll | 10 +++++----- .../security/CWE-730/RegexInjectionTest.java | 2 -- 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index 24520bab2302..66e315020b2d 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -43,7 +43,7 @@ private class RegexSanitizationCall extends RegexInjectionSanitizer { } /** - * A call to the `Pattern.quote` method, which gives metacharacters or escape sequences + * A call to the `Pattern.quote` method, which gives meta-characters or escape sequences * no special meaning. */ private class PatternQuoteCall extends RegexInjectionSanitizer { @@ -56,7 +56,7 @@ private class PatternQuoteCall extends RegexInjectionSanitizer { } /** - * Use of the `Pattern.LITERAL` flag with `Pattern.compile`, which gives metacharacters + * Use of the `Pattern.LITERAL` flag with `Pattern.compile`, which gives meta-characters * or escape sequences no special meaning. */ private class PatternLiteralFlag extends RegexInjectionSanitizer { @@ -72,7 +72,7 @@ private class PatternLiteralFlag extends RegexInjectionSanitizer { } /** - * The methods of the class `java.lang.String` that take a regular expression + * A method of the class `java.lang.String` that takes a regular expression * as a parameter. */ private class StringRegexMethod extends Method { @@ -83,7 +83,7 @@ private class StringRegexMethod extends Method { } /** - * The methods of the class `java.util.regex.Pattern` that take a regular + * A method of the class `java.util.regex.Pattern` that takes a regular * expression as a parameter. */ private class PatternRegexMethod extends Method { @@ -94,7 +94,7 @@ private class PatternRegexMethod extends Method { } /** - * The methods of the class `org.apache.commons.lang3.RegExUtils` that take + * A methods of the class `org.apache.commons.lang3.RegExUtils` that takes * a regular expression of type `String` as a parameter. */ private class ApacheRegExUtilsMethod extends Method { diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index e9d6a1d7f3e8..d78c8930e87c 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -199,5 +199,3 @@ public boolean literalTest(javax.servlet.http.HttpServletRequest request) { return Pattern.compile(pattern, Pattern.LITERAL).matcher(input).matches(); // Safe } } - -// ! see the following for potential additional test case ideas: https://www.baeldung.com/regular-expressions-java From be548c13e1baaeeee216ab1087380f1f7b15dcc9 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 3 Nov 2022 13:16:47 -0400 Subject: [PATCH 15/23] switch sink to use csv models --- .../lib/semmle/code/java/frameworks/Regex.qll | 6 +- .../code/java/frameworks/apache/Lang.qll | 5 -- .../code/java/regex/RegexFlowModels.qll | 6 ++ .../code/java/security/RegexInjection.qll | 83 ++++++------------- 4 files changed, 35 insertions(+), 65 deletions(-) diff --git a/java/ql/lib/semmle/code/java/frameworks/Regex.qll b/java/ql/lib/semmle/code/java/frameworks/Regex.qll index fc157634b9b6..790255c97031 100644 --- a/java/ql/lib/semmle/code/java/frameworks/Regex.qll +++ b/java/ql/lib/semmle/code/java/frameworks/Regex.qll @@ -11,13 +11,13 @@ class TypeRegexPattern extends Class { class PatternQuoteMethod extends Method { PatternQuoteMethod() { this.getDeclaringType() instanceof TypeRegexPattern and - this.hasName(["quote"]) + this.hasName("quote") } } /** The `LITERAL` field of the `java.util.regex.Pattern` class. */ -class PatternLiteral extends Field { - PatternLiteral() { +class PatternLiteralField extends Field { + PatternLiteralField() { this.getDeclaringType() instanceof TypeRegexPattern and this.hasName("LITERAL") } diff --git a/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll b/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll index d2c94bcb376d..84db672e9355 100644 --- a/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll +++ b/java/ql/lib/semmle/code/java/frameworks/apache/Lang.qll @@ -46,8 +46,3 @@ class TypeApacheSystemUtils extends Class { this.hasQualifiedName(["org.apache.commons.lang", "org.apache.commons.lang3"], "SystemUtils") } } - -/** The class `org.apache.commons.lang3.RegExUtils`. */ -class TypeApacheRegExUtils extends Class { - TypeApacheRegExUtils() { this.hasQualifiedName("org.apache.commons.lang3", "RegExUtils") } -} diff --git a/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll b/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll index de0b5465fe49..2e6be8b9bab1 100644 --- a/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll +++ b/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll @@ -27,6 +27,12 @@ private class RegexSinkCsv extends SinkModelCsv { "com.google.common.base;Splitter;false;split;(CharSequence);;Argument[-1];regex-use[0];manual", "com.google.common.base;Splitter;false;splitToList;(CharSequence);;Argument[-1];regex-use[0];manual", "com.google.common.base;Splitter$MapSplitter;false;split;(CharSequence);;Argument[-1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;removeAll;(String,String);;Argument[1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;removeFirst;(String,String);;Argument[1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;removePattern;(String,String);;Argument[1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;replaceAll;(String,String,String);;Argument[1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;replaceFirst;(String,String,String);;Argument[1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;replacePattern;(String,String,String);;Argument[1];regex-use[0];manual", ] } } diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index 66e315020b2d..2bfeeaeb43da 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -3,7 +3,8 @@ import java private import semmle.code.java.dataflow.DataFlow private import semmle.code.java.frameworks.Regex -private import semmle.code.java.frameworks.apache.Lang +//private import semmle.code.java.frameworks.apache.Lang +private import semmle.code.java.regex.RegexFlowModels /** A data flow sink for untrusted user input used to construct regular expressions. */ abstract class RegexInjectionSink extends DataFlow::ExprNode { } @@ -14,15 +15,11 @@ abstract class RegexInjectionSanitizer extends DataFlow::ExprNode { } /** A method call that takes a regular expression as an argument. */ private class DefaultRegexInjectionSink extends RegexInjectionSink { DefaultRegexInjectionSink() { - exists(MethodAccess ma, Method m | m = ma.getMethod() | - ma.getArgument(0) = this.asExpr() and - ( - m instanceof StringRegexMethod or - m instanceof PatternRegexMethod - ) - or - ma.getArgument(1) = this.asExpr() and - m instanceof ApacheRegExUtilsMethod + exists(string kind | + kind.matches([ + "regex-use[]", "regex-use[f1]", "regex-use[f-1]", "regex-use[-1]", "regex-use[0]" + ]) and + sinkNode(this, kind) ) } } @@ -30,20 +27,29 @@ private class DefaultRegexInjectionSink extends RegexInjectionSink { /** A call to a function whose name suggests that it escapes regular expression meta-characters. */ private class RegexSanitizationCall extends RegexInjectionSanitizer { RegexSanitizationCall() { - exists(string calleeName, string sanitize, string regexp | + // original + // 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 + + // ".*)") + // ) + // without regexp + exists(string calleeName, string sanitize | calleeName = this.asExpr().(Call).getCallee().getName() and - sanitize = "(?:escape|saniti[sz]e)" and - regexp = "regexp?" + sanitize = "(?:escape|saniti[sz]e)" | - calleeName - .regexpMatch("(?i)(" + sanitize + ".*" + regexp + ".*)" + "|(" + regexp + ".*" + sanitize + - ".*)") + calleeName.regexpMatch("(?i)(.*" + sanitize + ".*)") + //calleeName.matches("handleEscapes") ) } } /** - * A call to the `Pattern.quote` method, which gives meta-characters or escape sequences + * A call to the `Pattern.quote` method, which gives metacharacters or escape sequences * no special meaning. */ private class PatternQuoteCall extends RegexInjectionSanitizer { @@ -56,54 +62,17 @@ private class PatternQuoteCall extends RegexInjectionSanitizer { } /** - * Use of the `Pattern.LITERAL` flag with `Pattern.compile`, which gives meta-characters + * Use of the `Pattern.LITERAL` flag with `Pattern.compile`, which gives metacharacters * or escape sequences no special meaning. */ private class PatternLiteralFlag extends RegexInjectionSanitizer { PatternLiteralFlag() { exists(MethodAccess ma, Method m, Field field | m = ma.getMethod() | ma.getArgument(0) = this.asExpr() and - m instanceof PatternRegexMethod and + m.getDeclaringType() instanceof TypeRegexPattern and m.hasName("compile") and - field instanceof PatternLiteral and + field instanceof PatternLiteralField and ma.getArgument(1) = field.getAnAccess() ) } } - -/** - * A method of the class `java.lang.String` that takes a regular expression - * as a parameter. - */ -private class StringRegexMethod extends Method { - StringRegexMethod() { - this.getDeclaringType() instanceof TypeString and - this.hasName(["matches", "split", "replaceFirst", "replaceAll"]) - } -} - -/** - * A method of the class `java.util.regex.Pattern` that takes a regular - * expression as a parameter. - */ -private class PatternRegexMethod extends Method { - PatternRegexMethod() { - this.getDeclaringType() instanceof TypeRegexPattern and - this.hasName(["compile", "matches"]) - } -} - -/** - * A methods of the class `org.apache.commons.lang3.RegExUtils` that takes - * a regular expression of type `String` as a parameter. - */ -private class ApacheRegExUtilsMethod extends Method { - ApacheRegExUtilsMethod() { - this.getDeclaringType() instanceof TypeApacheRegExUtils and - // only handles String param here because the other param option, Pattern, is already handled by `java.util.regex.Pattern` - this.getParameterType(1) instanceof TypeString and - this.hasName([ - "removeAll", "removeFirst", "removePattern", "replaceAll", "replaceFirst", "replacePattern" - ]) - } -} From 54020013626ce0d973d26cf01e9b69712455585f Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 3 Nov 2022 13:25:08 -0400 Subject: [PATCH 16/23] remove original sanitizer --- .../code/java/security/RegexInjection.qll | 25 -------- .../security/CWE-730/RegexInjectionTest.java | 61 ------------------- 2 files changed, 86 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/RegexInjection.qll index 2bfeeaeb43da..859fac475d17 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/RegexInjection.qll @@ -3,7 +3,6 @@ import java private import semmle.code.java.dataflow.DataFlow private import semmle.code.java.frameworks.Regex -//private import semmle.code.java.frameworks.apache.Lang private import semmle.code.java.regex.RegexFlowModels /** A data flow sink for untrusted user input used to construct regular expressions. */ @@ -24,30 +23,6 @@ private class DefaultRegexInjectionSink extends RegexInjectionSink { } } -/** A call to a function whose name suggests that it escapes regular expression meta-characters. */ -private class RegexSanitizationCall extends RegexInjectionSanitizer { - RegexSanitizationCall() { - // original - // 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 + - // ".*)") - // ) - // without regexp - exists(string calleeName, string sanitize | - calleeName = this.asExpr().(Call).getCallee().getName() and - sanitize = "(?:escape|saniti[sz]e)" - | - calleeName.regexpMatch("(?i)(.*" + sanitize + ".*)") - //calleeName.matches("handleEscapes") - ) - } -} - /** * A call to the `Pattern.quote` method, which gives metacharacters or escape sequences * no special meaning. diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index d78c8930e87c..37ea99cd4f01 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -72,67 +72,6 @@ String foo(String str) { return str; } - public boolean pattern5(javax.servlet.http.HttpServletRequest request) { - String pattern = request.getParameter("pattern"); - String input = request.getParameter("input"); - - // Safe: User input is sanitized before constructing the regex - return input.matches("^" + escapeSpecialRegexChars(pattern) + "=.*$"); - } - - public boolean pattern6(javax.servlet.http.HttpServletRequest request) { - String pattern = request.getParameter("pattern"); - String input = request.getParameter("input"); - - escapeSpecialRegexChars(pattern); - - // BAD: the pattern is not really sanitized - return input.matches("^" + pattern + "=.*$"); // $ hasRegexInjection - } - - public boolean pattern7(javax.servlet.http.HttpServletRequest request) { - String pattern = request.getParameter("pattern"); - String input = request.getParameter("input"); - - String escapedPattern = escapeSpecialRegexChars(pattern); - - // Safe: User input is sanitized before constructing the regex - return input.matches("^" + escapedPattern + "=.*$"); - } - - public boolean pattern8(javax.servlet.http.HttpServletRequest request) { - String pattern = request.getParameter("pattern"); - String input = request.getParameter("input"); - - // Safe: User input is sanitized before constructing the regex - return input.matches("^" + sanitizeSpecialRegexChars(pattern) + "=.*$"); - } - - public boolean pattern9(javax.servlet.http.HttpServletRequest request) { - String pattern = request.getParameter("pattern"); - String input = request.getParameter("input"); - - // Safe: User input is sanitized before constructing the regex - return input.matches("^" + sanitiseSpecialRegexChars(pattern) + "=.*$"); - } - - Pattern SPECIAL_REGEX_CHARS = Pattern.compile("[{}()\\[\\]><-=!.+*?^$\\\\|]"); - - // test `escape...regex` - String escapeSpecialRegexChars(String str) { - return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); - } - - // test `sanitize...regex` - String sanitizeSpecialRegexChars(String str) { - return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); - } - - // test `sanitise...regex` - String sanitiseSpecialRegexChars(String str) { - return SPECIAL_REGEX_CHARS.matcher(str).replaceAll("\\\\$0"); - } - public boolean apache1(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); From 695d6f0e4e5389dbf6864017b5be9daeac7002df Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 3 Nov 2022 13:42:41 -0400 Subject: [PATCH 17/23] move files to regexp directory --- .../semmle/code/java/security/{ => regexp}/RegexInjection.qll | 0 .../code/java/security/{ => regexp}/RegexInjectionQuery.qll | 2 +- java/ql/src/Security/CWE/CWE-730/RegexInjection.ql | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename java/ql/lib/semmle/code/java/security/{ => regexp}/RegexInjection.qll (100%) rename java/ql/lib/semmle/code/java/security/{ => regexp}/RegexInjectionQuery.qll (92%) diff --git a/java/ql/lib/semmle/code/java/security/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll similarity index 100% rename from java/ql/lib/semmle/code/java/security/RegexInjection.qll rename to java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll diff --git a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll b/java/ql/lib/semmle/code/java/security/regexp/RegexInjectionQuery.qll similarity index 92% rename from java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll rename to java/ql/lib/semmle/code/java/security/regexp/RegexInjectionQuery.qll index 914b3222d9f7..e2453c08dcae 100644 --- a/java/ql/lib/semmle/code/java/security/RegexInjectionQuery.qll +++ b/java/ql/lib/semmle/code/java/security/regexp/RegexInjectionQuery.qll @@ -3,7 +3,7 @@ import java import semmle.code.java.dataflow.FlowSources import semmle.code.java.dataflow.TaintTracking -import semmle.code.java.security.RegexInjection +import semmle.code.java.security.regexp.RegexInjection /** A taint-tracking configuration for untrusted user input used to construct regular expressions. */ class RegexInjectionConfiguration extends TaintTracking::Configuration { diff --git a/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql index 0f0175b30cd8..820e0949d220 100644 --- a/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.ql @@ -14,7 +14,7 @@ */ import java -import semmle.code.java.security.RegexInjectionQuery +import semmle.code.java.security.regexp.RegexInjectionQuery import DataFlow::PathGraph from DataFlow::PathNode source, DataFlow::PathNode sink, RegexInjectionConfiguration c From 0e93e7112732e735d99e85c74910b7a5185a75ea Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 3 Nov 2022 14:14:32 -0400 Subject: [PATCH 18/23] update tests --- .../security/CWE-730/RegexInjectionTest.java | 18 ++++++++++++++++-- .../security/CWE-730/RegexInjectionTest.ql | 2 +- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index 37ea99cd4f01..57a1ea926679 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -27,13 +27,20 @@ public boolean string3(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return input.replaceFirst(pattern, "").length() > 0; // $ hasRegexInjection + return input.split(pattern, 0).length > 0; // $ hasRegexInjection } public boolean string4(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); + return input.replaceFirst(pattern, "").length() > 0; // $ hasRegexInjection + } + + public boolean string5(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + String input = request.getParameter("input"); + return input.replaceAll(pattern, "").length() > 0; // $ hasRegexInjection } @@ -58,13 +65,20 @@ public boolean pattern3(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return Pattern.matches(pattern, input); // $ hasRegexInjection + return Pattern.compile(pattern, 0).matcher(input).matches(); // $ hasRegexInjection } public boolean pattern4(javax.servlet.http.HttpServletRequest request) { String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); + return Pattern.matches(pattern, input); // $ hasRegexInjection + } + + public boolean pattern5(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + String input = request.getParameter("input"); + return input.matches("^" + foo(pattern) + "=.*$"); // $ hasRegexInjection } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql index 596decf98af9..9603ad37b8f4 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql @@ -1,6 +1,6 @@ import java import TestUtilities.InlineExpectationsTest -import semmle.code.java.security.RegexInjectionQuery +import semmle.code.java.security.regexp.RegexInjectionQuery //import semmle.code.java.security.regexp.PolynomialReDoSQuery class RegexInjectionTest extends InlineExpectationsTest { From e49c5213ca0b32ce38c65a5fb5df16c2db7b0758 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Thu, 3 Nov 2022 14:21:37 -0400 Subject: [PATCH 19/23] update change note --- java/ql/src/change-notes/2022-10-28-regex-injection.md | 4 ---- java/ql/src/change-notes/2022-11-03-regex-injection.md | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) delete mode 100644 java/ql/src/change-notes/2022-10-28-regex-injection.md create mode 100644 java/ql/src/change-notes/2022-11-03-regex-injection.md diff --git a/java/ql/src/change-notes/2022-10-28-regex-injection.md b/java/ql/src/change-notes/2022-10-28-regex-injection.md deleted file mode 100644 index f2376f34c8a0..000000000000 --- a/java/ql/src/change-notes/2022-10-28-regex-injection.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -category: newQuery ---- -* Added a new query, `java/regex-injection`, to detect unescaped user input used in regular expressions. diff --git a/java/ql/src/change-notes/2022-11-03-regex-injection.md b/java/ql/src/change-notes/2022-11-03-regex-injection.md new file mode 100644 index 000000000000..759aa2a86e1e --- /dev/null +++ b/java/ql/src/change-notes/2022-11-03-regex-injection.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* The query, `java/regex-injection`, has been promoted from experimental to the main query pack. Its results will now appear by default. This query was originally [submitted as an experimental query by @edvraa](https://github.com/github/codeql/pull/5704). From b99a1d2cd9924b7261a205eb82ecbcfb1ae2b160 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Fri, 4 Nov 2022 11:41:54 -0400 Subject: [PATCH 20/23] update sink and tests --- .../semmle/code/java/regex/RegexFlowModels.qll | 12 ++++++------ .../code/java/security/regexp/RegexInjection.qll | 4 +--- .../security/CWE-730/RegexInjectionTest.java | 16 ++++++++++++++-- 3 files changed, 21 insertions(+), 11 deletions(-) diff --git a/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll b/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll index 2e6be8b9bab1..20ba2c14dc8f 100644 --- a/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll +++ b/java/ql/lib/semmle/code/java/regex/RegexFlowModels.qll @@ -27,12 +27,12 @@ private class RegexSinkCsv extends SinkModelCsv { "com.google.common.base;Splitter;false;split;(CharSequence);;Argument[-1];regex-use[0];manual", "com.google.common.base;Splitter;false;splitToList;(CharSequence);;Argument[-1];regex-use[0];manual", "com.google.common.base;Splitter$MapSplitter;false;split;(CharSequence);;Argument[-1];regex-use[0];manual", - "org.apache.commons.lang3;RegExUtils;false;removeAll;(String,String);;Argument[1];regex-use[0];manual", - "org.apache.commons.lang3;RegExUtils;false;removeFirst;(String,String);;Argument[1];regex-use[0];manual", - "org.apache.commons.lang3;RegExUtils;false;removePattern;(String,String);;Argument[1];regex-use[0];manual", - "org.apache.commons.lang3;RegExUtils;false;replaceAll;(String,String,String);;Argument[1];regex-use[0];manual", - "org.apache.commons.lang3;RegExUtils;false;replaceFirst;(String,String,String);;Argument[1];regex-use[0];manual", - "org.apache.commons.lang3;RegExUtils;false;replacePattern;(String,String,String);;Argument[1];regex-use[0];manual", + "org.apache.commons.lang3;RegExUtils;false;removeAll;(String,String);;Argument[1];regex-use;manual", + "org.apache.commons.lang3;RegExUtils;false;removeFirst;(String,String);;Argument[1];regex-use;manual", + "org.apache.commons.lang3;RegExUtils;false;removePattern;(String,String);;Argument[1];regex-use;manual", + "org.apache.commons.lang3;RegExUtils;false;replaceAll;(String,String,String);;Argument[1];regex-use;manual", + "org.apache.commons.lang3;RegExUtils;false;replaceFirst;(String,String,String);;Argument[1];regex-use;manual", + "org.apache.commons.lang3;RegExUtils;false;replacePattern;(String,String,String);;Argument[1];regex-use;manual", ] } } diff --git a/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll index 859fac475d17..21dde6edb41d 100644 --- a/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll @@ -15,9 +15,7 @@ abstract class RegexInjectionSanitizer extends DataFlow::ExprNode { } private class DefaultRegexInjectionSink extends RegexInjectionSink { DefaultRegexInjectionSink() { exists(string kind | - kind.matches([ - "regex-use[]", "regex-use[f1]", "regex-use[f-1]", "regex-use[-1]", "regex-use[0]" - ]) and + kind.matches(["regex-use[]", "regex-use[f1]", "regex-use[f-1]", "regex-use[-1]", "regex-use"]) and sinkNode(this, kind) ) } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java index 57a1ea926679..5c7a3ca05746 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.java @@ -7,6 +7,7 @@ import javax.servlet.ServletException; import org.apache.commons.lang3.RegExUtils; +import com.google.common.base.Splitter; public class RegexInjectionTest extends HttpServlet { public boolean string1(javax.servlet.http.HttpServletRequest request) { @@ -138,10 +139,10 @@ public boolean apache7(javax.servlet.http.HttpServletRequest request) { // test `Pattern.quote` sanitizer public boolean quoteTest(javax.servlet.http.HttpServletRequest request) { - String regex = request.getParameter("regex"); + String pattern = request.getParameter("pattern"); String input = request.getParameter("input"); - return input.matches(Pattern.quote(regex)); // Safe + return input.matches(Pattern.quote(pattern)); // Safe } // test `Pattern.LITERAL` sanitizer @@ -151,4 +152,15 @@ public boolean literalTest(javax.servlet.http.HttpServletRequest request) { return Pattern.compile(pattern, Pattern.LITERAL).matcher(input).matches(); // Safe } + + public Splitter guava1(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + return Splitter.onPattern(pattern); // $ hasRegexInjection + } + + public Splitter guava2(javax.servlet.http.HttpServletRequest request) { + String pattern = request.getParameter("pattern"); + // sink is `Pattern.compile` + return Splitter.on(Pattern.compile(pattern)); // $ hasRegexInjection + } } From bada9864336f065a066ec14c34c989c0df094d14 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 8 Nov 2022 09:34:46 -0500 Subject: [PATCH 21/23] apply review comments --- .../ql/lib/semmle/code/java/security/regexp/RegexInjection.qll | 3 +-- .../ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll index 21dde6edb41d..4df736e62416 100644 --- a/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll @@ -40,11 +40,10 @@ private class PatternQuoteCall extends RegexInjectionSanitizer { */ private class PatternLiteralFlag extends RegexInjectionSanitizer { PatternLiteralFlag() { - exists(MethodAccess ma, Method m, Field field | m = ma.getMethod() | + exists(MethodAccess ma, Method m, PatternLiteralField field | m = ma.getMethod() | ma.getArgument(0) = this.asExpr() and m.getDeclaringType() instanceof TypeRegexPattern and m.hasName("compile") and - field instanceof PatternLiteralField and ma.getArgument(1) = field.getAnAccess() ) } diff --git a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql index 9603ad37b8f4..368b5170bfc4 100644 --- a/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql +++ b/java/ql/test/query-tests/security/CWE-730/RegexInjectionTest.ql @@ -2,7 +2,6 @@ import java import TestUtilities.InlineExpectationsTest import semmle.code.java.security.regexp.RegexInjectionQuery -//import semmle.code.java.security.regexp.PolynomialReDoSQuery class RegexInjectionTest extends InlineExpectationsTest { RegexInjectionTest() { this = "RegexInjectionTest" } From 13decd38d90d30449f0a875278c35d8a2d9f3772 Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Tue, 8 Nov 2022 15:27:37 -0500 Subject: [PATCH 22/23] update sink --- .../lib/semmle/code/java/security/regexp/RegexInjection.qll | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll index 4df736e62416..3c1e2e982296 100644 --- a/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll +++ b/java/ql/lib/semmle/code/java/security/regexp/RegexInjection.qll @@ -14,10 +14,8 @@ abstract class RegexInjectionSanitizer extends DataFlow::ExprNode { } /** A method call that takes a regular expression as an argument. */ private class DefaultRegexInjectionSink extends RegexInjectionSink { DefaultRegexInjectionSink() { - exists(string kind | - kind.matches(["regex-use[]", "regex-use[f1]", "regex-use[f-1]", "regex-use[-1]", "regex-use"]) and - sinkNode(this, kind) - ) + // we only select sinks where there is direct regex creation, not regex uses + sinkNode(this, ["regex-use[]", "regex-use[f1]", "regex-use[f-1]", "regex-use[-1]", "regex-use"]) } } From 9e2ec9d12f599408f20a28d036163316b6eb077a Mon Sep 17 00:00:00 2001 From: Jami Cogswell Date: Mon, 21 Nov 2022 13:39:46 -0500 Subject: [PATCH 23/23] apply docs review suggestion --- java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp b/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp index fc8ab33ca80a..3e239d07107b 100644 --- a/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp +++ b/java/ql/src/Security/CWE/CWE-730/RegexInjection.qhelp @@ -25,7 +25,7 @@ The following example shows an HTTP request parameter that is used to construct

    In the first case the user-provided regex is not escaped. -If a malicious user provides a regex that has exponential worst case performance, +If a malicious user provides a regex whose worst-case performance is exponential, then this could lead to a Denial of Service.