Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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.
9 changes: 6 additions & 3 deletions python/ql/lib/semmle/python/frameworks/Stdlib.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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()
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -164,15 +164,15 @@ 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
call =
API::moduleImport("re")
.getMember("compile")
.getReturn()
.getMember(["match", "fullmatch"])
.getMember(["match", "prefixmatch", "fullmatch"])
.getACall() and
strNode = [call.getArg(0), call.getArgByName("string")]
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import re
from re import prefixmatch as prefix_match

ts = TAINTED_STRING
pat = r"(?P<key>.*)"
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(),
)
5 changes: 5 additions & 0 deletions python/ql/test/library-tests/regex/Mode.expected
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,8 @@
| 63 | VERBOSE |
| 65 | ASCII |
| 77 | MULTILINE |
| 91 | IGNORECASE |
| 92 | DOTALL |
| 92 | MULTILINE |
| 93 | VERBOSE |
| 94 | IGNORECASE |
2 changes: 2 additions & 0 deletions python/ql/test/library-tests/regex/Regex.expected
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
8 changes: 7 additions & 1 deletion python/ql/test/library-tests/regex/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,4 +85,10 @@

# Anchors
re.compile(r'\Afoo\Z')
re.compile(r'\bfoo\B')
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)", "")
Original file line number Diff line number Diff line change
Expand Up @@ -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+ |
Original file line number Diff line number Diff line change
Expand Up @@ -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 | |
Expand All @@ -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 |
Expand All @@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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
Loading
Loading