From 000be49a8dc0584badd3ce4b5cbb87b178c292d5 Mon Sep 17 00:00:00 2001 From: Taus Date: Tue, 22 Sep 2026 14:24:39 +0000 Subject: [PATCH] Python: Model `prefixmatch` regular expression APIs This has essentially the same security implications as the existing `match`, so we just extend the existing modelling to also handle `prefixmatch`. --- .../2026-09-22-python315-prefixmatch.md | 4 ++ .../lib/semmle/python/frameworks/Stdlib.qll | 9 ++-- .../python/regexp/internal/ParseRegExp.qll | 2 +- ...ServerSideRequestForgeryCustomizations.qll | 4 +- .../frameworks/stdlib/test_re_prefixmatch.py | 46 +++++++++++++++++++ .../ql/test/library-tests/regex/Mode.expected | 5 ++ .../test/library-tests/regex/Regex.expected | 2 + python/ql/test/library-tests/regex/test.py | 8 +++- .../PolynomialBackTracking.expected | 3 ++ .../PolynomialReDoS.expected | 20 ++++++++ .../Security/CWE-730-PolynomialReDoS/test.py | 10 ++++ .../RegexInjection.expected | 46 +++++++++++++++++-- .../CWE-730-RegexInjection/re_prefixmatch.py | 30 ++++++++++++ .../FullServerSideRequestForgery.expected | 17 +++++++ .../PartialServerSideRequestForgery.expected | 29 ++++++++++++ .../test_re_prefixmatch.py | 37 +++++++++++++++ 16 files changed, 261 insertions(+), 11 deletions(-) create mode 100644 python/ql/lib/change-notes/2026-09-22-python315-prefixmatch.md create mode 100644 python/ql/test/library-tests/frameworks/stdlib/test_re_prefixmatch.py create mode 100644 python/ql/test/query-tests/Security/CWE-730-RegexInjection/re_prefixmatch.py create mode 100644 python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/test_re_prefixmatch.py diff --git a/python/ql/lib/change-notes/2026-09-22-python315-prefixmatch.md b/python/ql/lib/change-notes/2026-09-22-python315-prefixmatch.md new file mode 100644 index 000000000000..1dc703dc48c6 --- /dev/null +++ b/python/ql/lib/change-notes/2026-09-22-python315-prefixmatch.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added modeling for Python 3.15's `re.prefixmatch` and `re.Pattern.prefixmatch`, matching the existing support for their `match` counterparts. \ No newline at end of file diff --git a/python/ql/lib/semmle/python/frameworks/Stdlib.qll b/python/ql/lib/semmle/python/frameworks/Stdlib.qll index c02aa4bb6d8a..717a4de9c86a 100644 --- a/python/ql/lib/semmle/python/frameworks/Stdlib.qll +++ b/python/ql/lib/semmle/python/frameworks/Stdlib.qll @@ -3105,12 +3105,15 @@ module StdlibPrivate { */ private class RegexExecutionMethod extends string { RegexExecutionMethod() { - this in ["match", "fullmatch", "search", "split", "findall", "finditer", "sub", "subn"] + this in [ + "match", "prefixmatch", "fullmatch", "search", "split", "findall", "finditer", "sub", + "subn" + ] } /** Gets the index of the argument representing the string to be searched by a regex. */ int getStringArgIndex() { - this in ["match", "fullmatch", "search", "split", "findall", "finditer"] and + this in ["match", "prefixmatch", "fullmatch", "search", "split", "findall", "finditer"] and result = 1 or this in ["sub", "subn"] and @@ -3244,7 +3247,7 @@ module StdlibPrivate { this = "compiled re.Match" ) | - result = re.getMember(["match", "search", "fullmatch"]).getACall() + result = re.getMember(["match", "prefixmatch", "search", "fullmatch"]).getACall() ) } diff --git a/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll b/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll index d91c4bbd78c0..0b47432cb16e 100644 --- a/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll +++ b/python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll @@ -44,7 +44,7 @@ private module FindRegexMode { or name = "search" and result = 2 or - name = "match" and result = 2 + name in ["match", "prefixmatch"] and result = 2 or name = "split" and result = 3 or diff --git a/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll index e3f18170f630..d1b649118cb1 100644 --- a/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll @@ -164,7 +164,7 @@ module ServerSideRequestForgery { ["isalnum", "isalpha", "isdecimal", "isdigit", "isidentifier", "isnumeric", "isspace"]) or branch = true and - call = API::moduleImport("re").getMember(["match", "fullmatch"]).getACall() and + call = API::moduleImport("re").getMember(["match", "prefixmatch", "fullmatch"]).getACall() and strNode = [call.getArg(1), call.getArgByName("string")] or branch = true and @@ -172,7 +172,7 @@ module ServerSideRequestForgery { API::moduleImport("re") .getMember("compile") .getReturn() - .getMember(["match", "fullmatch"]) + .getMember(["match", "prefixmatch", "fullmatch"]) .getACall() and strNode = [call.getArg(0), call.getArgByName("string")] ) diff --git a/python/ql/test/library-tests/frameworks/stdlib/test_re_prefixmatch.py b/python/ql/test/library-tests/frameworks/stdlib/test_re_prefixmatch.py new file mode 100644 index 000000000000..7a80fae545bf --- /dev/null +++ b/python/ql/test/library-tests/frameworks/stdlib/test_re_prefixmatch.py @@ -0,0 +1,46 @@ +import re +from re import prefixmatch as prefix_match + +ts = TAINTED_STRING +pat = r"(?P.*)" +compiled_pat = re.compile(pat) + +ensure_tainted( + re.prefixmatch(pat, ts), # $ tainted + re.prefixmatch(pattern=pat, string=ts), # $ tainted + prefix_match(pat, ts), # $ tainted + compiled_pat.prefixmatch(ts), # $ tainted + compiled_pat.prefixmatch(string=ts, pos=0, endpos=10), # $ tainted + + re.prefixmatch(pat, ts).string, # $ tainted + re.prefixmatch(ts, "safe").re.pattern, # $ tainted + compiled_pat.prefixmatch(ts).string, # $ tainted + re.compile(ts).prefixmatch("safe").re.pattern, # $ tainted +) + +direct_match = re.prefixmatch(pat, ts) +compiled_match = compiled_pat.prefixmatch(ts) +ensure_tainted( + direct_match.group(), # $ tainted + direct_match.groups()[0], # $ tainted + direct_match.groupdict()["key"], # $ tainted + direct_match[0], # $ tainted + direct_match["key"], # $ tainted + direct_match.expand(r"\1"), # $ tainted + + compiled_match.group(), # $ tainted + compiled_match.groups()[0], # $ tainted + compiled_match.groupdict()["key"], # $ tainted + compiled_match[0], # $ tainted + compiled_match["key"], # $ tainted + compiled_match.expand(r"\1"), # $ tainted +) + +ensure_not_tainted( + re.prefixmatch(pat, "safe").string, + re.prefixmatch(pat, ts).re.pattern, + re.prefixmatch(ts, "safe").group(), + compiled_pat.prefixmatch("safe").string, + compiled_pat.prefixmatch(ts).re.pattern, + re.compile(ts).prefixmatch("safe").group(), +) diff --git a/python/ql/test/library-tests/regex/Mode.expected b/python/ql/test/library-tests/regex/Mode.expected index 5640d028757c..406f41547d5c 100644 --- a/python/ql/test/library-tests/regex/Mode.expected +++ b/python/ql/test/library-tests/regex/Mode.expected @@ -26,3 +26,8 @@ | 63 | VERBOSE | | 65 | ASCII | | 77 | MULTILINE | +| 91 | IGNORECASE | +| 92 | DOTALL | +| 92 | MULTILINE | +| 93 | VERBOSE | +| 94 | IGNORECASE | diff --git a/python/ql/test/library-tests/regex/Regex.expected b/python/ql/test/library-tests/regex/Regex.expected index 00c80d5cc1cb..7e14ee12ead0 100644 --- a/python/ql/test/library-tests/regex/Regex.expected +++ b/python/ql/test/library-tests/regex/Regex.expected @@ -108,6 +108,8 @@ | (?aimsx:a+) | qualified | 8 | 10 | | (?aimsx:a+) | sequence | 0 | 11 | | (?aimsx:a+) | sequence | 7 | 10 | +| (?i) | empty group | 0 | 4 | +| (?i) | sequence | 0 | 4 | | (?m)^(?!$) | $ | 8 | 9 | | (?m)^(?!$) | ^ | 4 | 5 | | (?m)^(?!$) | empty group | 0 | 4 | diff --git a/python/ql/test/library-tests/regex/test.py b/python/ql/test/library-tests/regex/test.py index 8565e347781d..afb68aa2078a 100644 --- a/python/ql/test/library-tests/regex/test.py +++ b/python/ql/test/library-tests/regex/test.py @@ -85,4 +85,10 @@ # Anchors re.compile(r'\Afoo\Z') -re.compile(r'\bfoo\B') \ No newline at end of file +re.compile(r'\bfoo\B') + +# Python 3.15 prefixmatch flags +re.prefixmatch("", "", re.IGNORECASE) +re.prefixmatch("", "", flags=re.DOTALL | re.MULTILINE) +re.prefixmatch(pattern="", string="", flags=re.VERBOSE) +re.prefixmatch("(?i)", "") \ No newline at end of file diff --git a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialBackTracking.expected b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialBackTracking.expected index ba97e881e467..9798fb27eb22 100644 --- a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialBackTracking.expected +++ b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialBackTracking.expected @@ -3,3 +3,6 @@ | test.py:11:22:11:33 | StringLiteral | test.py:11:29:11:31 | \\s+ | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding \\s+$ | | test.py:18:14:18:25 | StringLiteral | test.py:18:21:18:23 | \\s+ | Strings with many repetitions of ' ' can start matching anywhere after the start of the preceeding \\s+$ | | test.py:20:23:20:274 | StringLiteral | test.py:20:271:20:272 | .* | Strings starting with 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC' and with many repetitions of 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC' can start matching anywhere after the start of the preceeding (AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)(AA\|BB)C.*Y | +| test.py:27:20:27:35 | StringLiteral | test.py:27:31:27:33 | \\d+ | Strings starting with '0.9' and with many repetitions of '99' can start matching anywhere after the start of the preceeding \\d+ | +| test.py:28:28:28:43 | StringLiteral | test.py:28:39:28:41 | \\d+ | Strings starting with '0.9' and with many repetitions of '99' can start matching anywhere after the start of the preceeding \\d+ | +| test.py:30:26:30:41 | StringLiteral | test.py:30:37:30:39 | \\d+ | Strings starting with '0.9' and with many repetitions of '99' can start matching anywhere after the start of the preceeding \\d+ | diff --git a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.expected b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.expected index 22fcfd05fa19..4f9ed6be52b1 100644 --- a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.expected +++ b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.expected @@ -4,9 +4,14 @@ | test.py:12:17:12:20 | ControlFlowNode for text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:12:17:12:20 | ControlFlowNode for text | This $@ that depends on a $@ may run slow on strings with many repetitions of ' '. | test.py:11:29:11:31 | \\s+ | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | | test.py:16:24:16:30 | ControlFlowNode for my_text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:16:24:16:30 | ControlFlowNode for my_text | This $@ that depends on a $@ may run slow on strings with many repetitions of ' '. | test.py:18:21:18:23 | \\s+ | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | | test.py:21:18:21:21 | ControlFlowNode for text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:21:18:21:21 | ControlFlowNode for text | This $@ that depends on a $@ may run slow on strings starting with 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC' and with many repetitions of 'AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAC'. | test.py:20:271:20:272 | .* | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | +| test.py:27:38:27:41 | ControlFlowNode for text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:27:38:27:41 | ControlFlowNode for text | This $@ that depends on a $@ may run slow on strings starting with '0.9' and with many repetitions of '99'. | test.py:27:31:27:33 | \\d+ | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | +| test.py:28:53:28:56 | ControlFlowNode for text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:28:53:28:56 | ControlFlowNode for text | This $@ that depends on a $@ may run slow on strings starting with '0.9' and with many repetitions of '99'. | test.py:28:39:28:41 | \\d+ | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | +| test.py:31:25:31:28 | ControlFlowNode for text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:31:25:31:28 | ControlFlowNode for text | This $@ that depends on a $@ may run slow on strings starting with '0.9' and with many repetitions of '99'. | test.py:30:37:30:39 | \\d+ | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | +| test.py:32:32:32:35 | ControlFlowNode for text | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:32:32:32:35 | ControlFlowNode for text | This $@ that depends on a $@ may run slow on strings starting with '0.9' and with many repetitions of '99'. | test.py:30:37:30:39 | \\d+ | regular expression | test.py:2:26:2:32 | ControlFlowNode for ImportMember | user-provided value | edges | test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:2:26:2:32 | ControlFlowNode for request | provenance | | | test.py:2:26:2:32 | ControlFlowNode for request | test.py:7:12:7:18 | ControlFlowNode for request | provenance | | +| test.py:2:26:2:32 | ControlFlowNode for request | test.py:26:12:26:18 | ControlFlowNode for request | provenance | | | test.py:7:5:7:8 | ControlFlowNode for text | test.py:8:30:8:33 | ControlFlowNode for text | provenance | | | test.py:7:5:7:8 | ControlFlowNode for text | test.py:9:32:9:35 | ControlFlowNode for text | provenance | | | test.py:7:5:7:8 | ControlFlowNode for text | test.py:12:17:12:20 | ControlFlowNode for text | provenance | | @@ -17,6 +22,13 @@ edges | test.py:7:12:7:35 | ControlFlowNode for Attribute() | test.py:7:5:7:8 | ControlFlowNode for text | provenance | | | test.py:14:33:14:39 | ControlFlowNode for my_text | test.py:16:24:16:30 | ControlFlowNode for my_text | provenance | | | test.py:18:28:18:31 | ControlFlowNode for text | test.py:14:33:14:39 | ControlFlowNode for my_text | provenance | | +| test.py:26:5:26:8 | ControlFlowNode for text | test.py:27:38:27:41 | ControlFlowNode for text | provenance | | +| test.py:26:5:26:8 | ControlFlowNode for text | test.py:28:53:28:56 | ControlFlowNode for text | provenance | | +| test.py:26:5:26:8 | ControlFlowNode for text | test.py:31:25:31:28 | ControlFlowNode for text | provenance | | +| test.py:26:5:26:8 | ControlFlowNode for text | test.py:32:32:32:35 | ControlFlowNode for text | provenance | | +| test.py:26:12:26:18 | ControlFlowNode for request | test.py:26:12:26:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep | +| test.py:26:12:26:23 | ControlFlowNode for Attribute | test.py:26:12:26:35 | ControlFlowNode for Attribute() | provenance | dict.get | +| test.py:26:12:26:35 | ControlFlowNode for Attribute() | test.py:26:5:26:8 | ControlFlowNode for text | provenance | | nodes | test.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | test.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | @@ -31,4 +43,12 @@ nodes | test.py:16:24:16:30 | ControlFlowNode for my_text | semmle.label | ControlFlowNode for my_text | | test.py:18:28:18:31 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | | test.py:21:18:21:21 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | +| test.py:26:5:26:8 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | +| test.py:26:12:26:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test.py:26:12:26:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | +| test.py:26:12:26:35 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | +| test.py:27:38:27:41 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | +| test.py:28:53:28:56 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | +| test.py:31:25:31:28 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | +| test.py:32:32:32:35 | ControlFlowNode for text | semmle.label | ControlFlowNode for text | subpaths diff --git a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/test.py b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/test.py index 60584229b917..5ed2f22c3657 100644 --- a/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/test.py +++ b/python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/test.py @@ -20,3 +20,13 @@ def indirect(input_reg_str, my_text): reg2 = re.compile(r"(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)(AA|BB)C.*Y") reg2.sub("", text) # $ Alert # NOT OK + +@app.route("/prefixmatch-poly-redos") +def prefixmatch(): + text = request.args.get("text") + re.prefixmatch(r"^0\.\d+E?\d+$", text) # $ Alert + re.prefixmatch(pattern=r"^0\.\d+E?\d+$", string=text) # $ Alert + + pattern = re.compile(r"^0\.\d+E?\d+$") + pattern.prefixmatch(text) # $ Alert + pattern.prefixmatch(string=text) # $ Alert diff --git a/python/ql/test/query-tests/Security/CWE-730-RegexInjection/RegexInjection.expected b/python/ql/test/query-tests/Security/CWE-730-RegexInjection/RegexInjection.expected index 16d29401f785..c825e2ec9deb 100644 --- a/python/ql/test/query-tests/Security/CWE-730-RegexInjection/RegexInjection.expected +++ b/python/ql/test/query-tests/Security/CWE-730-RegexInjection/RegexInjection.expected @@ -1,3 +1,12 @@ +#select +| re_bad.py:14:15:14:28 | ControlFlowNode for unsafe_pattern | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:14:15:14:28 | ControlFlowNode for unsafe_pattern | This regular expression depends on a $@ and is executed by $@. | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_bad.py:14:5:14:33 | ControlFlowNode for Attribute() | re.search | +| re_bad.py:25:35:25:48 | ControlFlowNode for unsafe_pattern | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:25:35:25:48 | ControlFlowNode for unsafe_pattern | This regular expression depends on a $@ and is executed by $@. | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_bad.py:26:5:26:31 | ControlFlowNode for Attribute() | re.search | +| re_bad.py:37:16:37:29 | ControlFlowNode for unsafe_pattern | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:37:16:37:29 | ControlFlowNode for unsafe_pattern | This regular expression depends on a $@ and is executed by $@. | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_bad.py:37:5:37:41 | ControlFlowNode for Attribute() | re.search | +| re_prefixmatch.py:8:20:8:26 | ControlFlowNode for pattern | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | re_prefixmatch.py:8:20:8:26 | ControlFlowNode for pattern | This regular expression depends on a $@ and is executed by $@. | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_prefixmatch.py:8:5:8:35 | ControlFlowNode for Attribute() | re.prefixmatch | +| re_prefixmatch.py:13:28:13:34 | ControlFlowNode for pattern | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | re_prefixmatch.py:13:28:13:34 | ControlFlowNode for pattern | This regular expression depends on a $@ and is executed by $@. | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_prefixmatch.py:13:5:13:50 | ControlFlowNode for Attribute() | re.prefixmatch | +| re_prefixmatch.py:18:18:18:24 | ControlFlowNode for pattern | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | re_prefixmatch.py:18:18:18:24 | ControlFlowNode for pattern | This regular expression depends on a $@ and is executed by $@. | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_prefixmatch.py:18:5:18:33 | ControlFlowNode for prefix_match() | re.prefixmatch | +| re_prefixmatch.py:23:35:23:41 | ControlFlowNode for pattern | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | re_prefixmatch.py:23:35:23:41 | ControlFlowNode for pattern | This regular expression depends on a $@ and is executed by $@. | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_prefixmatch.py:24:5:24:40 | ControlFlowNode for Attribute() | re.prefixmatch | +| re_prefixmatch.py:29:43:29:49 | ControlFlowNode for pattern | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | re_prefixmatch.py:29:43:29:49 | ControlFlowNode for pattern | This regular expression depends on a $@ and is executed by $@. | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_prefixmatch.py:30:5:30:64 | ControlFlowNode for Attribute() | re.prefixmatch | edges | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:1:19:1:25 | ControlFlowNode for request | provenance | | | re_bad.py:1:19:1:25 | ControlFlowNode for request | re_bad.py:13:22:13:28 | ControlFlowNode for request | provenance | | @@ -9,6 +18,22 @@ edges | re_bad.py:24:22:24:28 | ControlFlowNode for request | re_bad.py:24:5:24:18 | ControlFlowNode for unsafe_pattern | provenance | AdditionalTaintStep | | re_bad.py:36:5:36:18 | ControlFlowNode for unsafe_pattern | re_bad.py:37:16:37:29 | ControlFlowNode for unsafe_pattern | provenance | | | re_bad.py:36:22:36:28 | ControlFlowNode for request | re_bad.py:36:5:36:18 | ControlFlowNode for unsafe_pattern | provenance | AdditionalTaintStep | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | provenance | | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | re_prefixmatch.py:7:15:7:21 | ControlFlowNode for request | provenance | | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | re_prefixmatch.py:12:15:12:21 | ControlFlowNode for request | provenance | | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | re_prefixmatch.py:17:15:17:21 | ControlFlowNode for request | provenance | | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | re_prefixmatch.py:22:15:22:21 | ControlFlowNode for request | provenance | | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | re_prefixmatch.py:28:15:28:21 | ControlFlowNode for request | provenance | | +| re_prefixmatch.py:7:5:7:11 | ControlFlowNode for pattern | re_prefixmatch.py:8:20:8:26 | ControlFlowNode for pattern | provenance | | +| re_prefixmatch.py:7:15:7:21 | ControlFlowNode for request | re_prefixmatch.py:7:5:7:11 | ControlFlowNode for pattern | provenance | AdditionalTaintStep | +| re_prefixmatch.py:12:5:12:11 | ControlFlowNode for pattern | re_prefixmatch.py:13:28:13:34 | ControlFlowNode for pattern | provenance | | +| re_prefixmatch.py:12:15:12:21 | ControlFlowNode for request | re_prefixmatch.py:12:5:12:11 | ControlFlowNode for pattern | provenance | AdditionalTaintStep | +| re_prefixmatch.py:17:5:17:11 | ControlFlowNode for pattern | re_prefixmatch.py:18:18:18:24 | ControlFlowNode for pattern | provenance | | +| re_prefixmatch.py:17:15:17:21 | ControlFlowNode for request | re_prefixmatch.py:17:5:17:11 | ControlFlowNode for pattern | provenance | AdditionalTaintStep | +| re_prefixmatch.py:22:5:22:11 | ControlFlowNode for pattern | re_prefixmatch.py:23:35:23:41 | ControlFlowNode for pattern | provenance | | +| re_prefixmatch.py:22:15:22:21 | ControlFlowNode for request | re_prefixmatch.py:22:5:22:11 | ControlFlowNode for pattern | provenance | AdditionalTaintStep | +| re_prefixmatch.py:28:5:28:11 | ControlFlowNode for pattern | re_prefixmatch.py:29:43:29:49 | ControlFlowNode for pattern | provenance | | +| re_prefixmatch.py:28:15:28:21 | ControlFlowNode for request | re_prefixmatch.py:28:5:28:11 | ControlFlowNode for pattern | provenance | AdditionalTaintStep | nodes | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | re_bad.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | @@ -21,8 +46,21 @@ nodes | re_bad.py:36:5:36:18 | ControlFlowNode for unsafe_pattern | semmle.label | ControlFlowNode for unsafe_pattern | | re_bad.py:36:22:36:28 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | | re_bad.py:37:16:37:29 | ControlFlowNode for unsafe_pattern | semmle.label | ControlFlowNode for unsafe_pattern | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | +| re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| re_prefixmatch.py:7:5:7:11 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:7:15:7:21 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| re_prefixmatch.py:8:20:8:26 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:12:5:12:11 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:12:15:12:21 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| re_prefixmatch.py:13:28:13:34 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:17:5:17:11 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:17:15:17:21 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| re_prefixmatch.py:18:18:18:24 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:22:5:22:11 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:22:15:22:21 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| re_prefixmatch.py:23:35:23:41 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:28:5:28:11 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | +| re_prefixmatch.py:28:15:28:21 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| re_prefixmatch.py:29:43:29:49 | ControlFlowNode for pattern | semmle.label | ControlFlowNode for pattern | subpaths -#select -| re_bad.py:14:15:14:28 | ControlFlowNode for unsafe_pattern | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:14:15:14:28 | ControlFlowNode for unsafe_pattern | This regular expression depends on a $@ and is executed by $@. | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_bad.py:14:5:14:33 | ControlFlowNode for Attribute() | re.search | -| re_bad.py:25:35:25:48 | ControlFlowNode for unsafe_pattern | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:25:35:25:48 | ControlFlowNode for unsafe_pattern | This regular expression depends on a $@ and is executed by $@. | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_bad.py:26:5:26:31 | ControlFlowNode for Attribute() | re.search | -| re_bad.py:37:16:37:29 | ControlFlowNode for unsafe_pattern | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | re_bad.py:37:16:37:29 | ControlFlowNode for unsafe_pattern | This regular expression depends on a $@ and is executed by $@. | re_bad.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | re_bad.py:37:5:37:41 | ControlFlowNode for Attribute() | re.search | diff --git a/python/ql/test/query-tests/Security/CWE-730-RegexInjection/re_prefixmatch.py b/python/ql/test/query-tests/Security/CWE-730-RegexInjection/re_prefixmatch.py new file mode 100644 index 000000000000..0badba8b1675 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-730-RegexInjection/re_prefixmatch.py @@ -0,0 +1,30 @@ +from flask import request # $ Source +import re +from re import prefixmatch as prefix_match + + +def direct(): + pattern = request.args["pattern"] + re.prefixmatch(pattern, "safe") # $ Alert + + +def direct_keywords(): + pattern = request.args["pattern"] + re.prefixmatch(pattern=pattern, string="safe") # $ Alert + + +def imported_alias(): + pattern = request.args["pattern"] + prefix_match(pattern, "safe") # $ Alert + + +def compiled(): + pattern = request.args["pattern"] + compiled_pattern = re.compile(pattern) # $ Alert + compiled_pattern.prefixmatch("safe") + + +def compiled_keywords(): + pattern = request.args["pattern"] + compiled_pattern = re.compile(pattern=pattern) # $ Alert + compiled_pattern.prefixmatch(string="safe", pos=0, endpos=4) diff --git a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected index 7434eca6978b..1fc30af2a40e 100644 --- a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected +++ b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/FullServerSideRequestForgery.expected @@ -39,6 +39,8 @@ | test_path_validation.py:122:9:122:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:122:32:122:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | | test_path_validation.py:125:9:125:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:125:32:125:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | | test_path_validation.py:132:9:132:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | +| test_re_prefixmatch.py:19:9:19:25 | ControlFlowNode for Attribute() | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | test_re_prefixmatch.py:19:22:19:24 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| test_re_prefixmatch.py:28:9:28:25 | ControlFlowNode for Attribute() | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | test_re_prefixmatch.py:28:22:28:24 | ControlFlowNode for url | The full URL of this request depends on a $@. | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | | test_requests.py:9:5:9:28 | ControlFlowNode for Attribute() | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:9:18:9:27 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | | test_requests.py:17:5:17:27 | ControlFlowNode for Attribute() | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:17:17:17:26 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | | test_requests.py:22:5:22:44 | ControlFlowNode for Attribute() | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:22:34:22:43 | ControlFlowNode for user_input | The full URL of this request depends on a $@. | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | @@ -138,6 +140,13 @@ edges | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:122:32:122:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:125:32:125:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | test_re_prefixmatch.py:15:11:15:17 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | test_re_prefixmatch.py:23:11:23:17 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:15:5:15:7 | ControlFlowNode for url | test_re_prefixmatch.py:19:22:19:24 | ControlFlowNode for url | provenance | | +| test_re_prefixmatch.py:15:11:15:17 | ControlFlowNode for request | test_re_prefixmatch.py:15:5:15:7 | ControlFlowNode for url | provenance | AdditionalTaintStep | +| test_re_prefixmatch.py:23:5:23:7 | ControlFlowNode for url | test_re_prefixmatch.py:28:22:28:24 | ControlFlowNode for url | provenance | | +| test_re_prefixmatch.py:23:11:23:17 | ControlFlowNode for request | test_re_prefixmatch.py:23:5:23:7 | ControlFlowNode for url | provenance | AdditionalTaintStep | | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:1:19:1:25 | ControlFlowNode for request | provenance | | | test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:7:18:7:24 | ControlFlowNode for request | provenance | | | test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:14:18:14:24 | ControlFlowNode for request | provenance | | @@ -249,6 +258,14 @@ nodes | test_path_validation.py:122:32:122:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | | test_path_validation.py:125:32:125:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:15:5:15:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:15:11:15:17 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:19:22:19:24 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:23:5:23:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:23:11:23:17 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:28:22:28:24 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | test_requests.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | | test_requests.py:7:5:7:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | diff --git a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected index a8d907793129..eb1d75c0e48d 100644 --- a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected +++ b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/PartialServerSideRequestForgery.expected @@ -39,6 +39,8 @@ | test_path_validation.py:120:9:120:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:120:32:120:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | | test_path_validation.py:127:9:127:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:127:32:127:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | | test_path_validation.py:130:9:130:58 | ControlFlowNode for SecretClient() | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | test_path_validation.py:130:32:130:34 | ControlFlowNode for url | Part of the URL of this request depends on a $@. | test_path_validation.py:5:19:5:25 | ControlFlowNode for ImportMember | user-provided value | +| test_re_prefixmatch.py:11:9:11:51 | ControlFlowNode for Attribute() | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | test_re_prefixmatch.py:11:22:11:50 | ControlFlowNode for Fstring | Part of the URL of this request depends on a $@. | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | +| test_re_prefixmatch.py:37:9:37:51 | ControlFlowNode for Attribute() | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | test_re_prefixmatch.py:37:22:37:50 | ControlFlowNode for Fstring | Part of the URL of this request depends on a $@. | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | user-provided value | edges | full_partial_test.py:1:19:1:25 | ControlFlowNode for ImportMember | full_partial_test.py:1:19:1:25 | ControlFlowNode for request | provenance | | | full_partial_test.py:1:19:1:25 | ControlFlowNode for request | full_partial_test.py:7:18:7:24 | ControlFlowNode for request | provenance | | @@ -226,6 +228,19 @@ edges | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:127:32:127:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:130:32:130:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | | test_path_validation.py:61:5:61:7 | ControlFlowNode for url | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | provenance | Sink:MaD:2 | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | test_re_prefixmatch.py:7:12:7:18 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | test_re_prefixmatch.py:15:11:15:17 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | test_re_prefixmatch.py:23:11:23:17 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | test_re_prefixmatch.py:32:12:32:18 | ControlFlowNode for request | provenance | | +| test_re_prefixmatch.py:7:5:7:8 | ControlFlowNode for path | test_re_prefixmatch.py:11:22:11:50 | ControlFlowNode for Fstring | provenance | | +| test_re_prefixmatch.py:7:12:7:18 | ControlFlowNode for request | test_re_prefixmatch.py:7:5:7:8 | ControlFlowNode for path | provenance | AdditionalTaintStep | +| test_re_prefixmatch.py:15:5:15:7 | ControlFlowNode for url | test_re_prefixmatch.py:19:22:19:24 | ControlFlowNode for url | provenance | | +| test_re_prefixmatch.py:15:11:15:17 | ControlFlowNode for request | test_re_prefixmatch.py:15:5:15:7 | ControlFlowNode for url | provenance | AdditionalTaintStep | +| test_re_prefixmatch.py:23:5:23:7 | ControlFlowNode for url | test_re_prefixmatch.py:28:22:28:24 | ControlFlowNode for url | provenance | | +| test_re_prefixmatch.py:23:11:23:17 | ControlFlowNode for request | test_re_prefixmatch.py:23:5:23:7 | ControlFlowNode for url | provenance | AdditionalTaintStep | +| test_re_prefixmatch.py:32:5:32:8 | ControlFlowNode for path | test_re_prefixmatch.py:37:22:37:50 | ControlFlowNode for Fstring | provenance | | +| test_re_prefixmatch.py:32:12:32:18 | ControlFlowNode for request | test_re_prefixmatch.py:32:5:32:8 | ControlFlowNode for path | provenance | AdditionalTaintStep | | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | test_requests.py:1:19:1:25 | ControlFlowNode for request | provenance | | | test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:7:18:7:24 | ControlFlowNode for request | provenance | | | test_requests.py:1:19:1:25 | ControlFlowNode for request | test_requests.py:14:18:14:24 | ControlFlowNode for request | provenance | | @@ -418,6 +433,20 @@ nodes | test_path_validation.py:127:32:127:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | | test_path_validation.py:130:32:130:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | | test_path_validation.py:132:32:132:34 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | +| test_re_prefixmatch.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:7:5:7:8 | ControlFlowNode for path | semmle.label | ControlFlowNode for path | +| test_re_prefixmatch.py:7:12:7:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:11:22:11:50 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring | +| test_re_prefixmatch.py:15:5:15:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:15:11:15:17 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:19:22:19:24 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:23:5:23:7 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:23:11:23:17 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:28:22:28:24 | ControlFlowNode for url | semmle.label | ControlFlowNode for url | +| test_re_prefixmatch.py:32:5:32:8 | ControlFlowNode for path | semmle.label | ControlFlowNode for path | +| test_re_prefixmatch.py:32:12:32:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | +| test_re_prefixmatch.py:37:22:37:50 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring | | test_requests.py:1:19:1:25 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember | | test_requests.py:1:19:1:25 | ControlFlowNode for request | semmle.label | ControlFlowNode for request | | test_requests.py:7:5:7:14 | ControlFlowNode for user_input | semmle.label | ControlFlowNode for user_input | diff --git a/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/test_re_prefixmatch.py b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/test_re_prefixmatch.py new file mode 100644 index 000000000000..6853413c3345 --- /dev/null +++ b/python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/test_re_prefixmatch.py @@ -0,0 +1,37 @@ +from flask import request # $ Source +import re +import requests + + +def direct(): + path = request.args["path"] + if re.prefixmatch(r"[a-zA-Z0-9]+\Z", path): + requests.get(f"https://example.com/{path}") + else: + requests.get(f"https://example.com/{path}") # $ Alert[py/partial-ssrf] + + +def direct_keywords(): + url = request.args["url"] + if re.prefixmatch(pattern=r"https://example\.com/[a-zA-Z0-9]+\Z", string=url): + requests.get(url) + else: + requests.get(url) # $ Alert[py/full-ssrf] + + +def compiled(): + url = request.args["url"] + pattern = re.compile(r"https://example\.com/[a-zA-Z0-9]+\Z") + if pattern.prefixmatch(url): + requests.get(url) + else: + requests.get(url) # $ Alert[py/full-ssrf] + + +def compiled_keywords(): + path = request.args["path"] + pattern = re.compile(r"[a-zA-Z0-9]+\Z") + if pattern.prefixmatch(string=path): + requests.get(f"https://example.com/{path}") + else: + requests.get(f"https://example.com/{path}") # $ Alert[py/partial-ssrf]