Skip to content

Commit 000be49

Browse files
committed
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`.
1 parent 74211cc commit 000be49

16 files changed

Lines changed: 261 additions & 11 deletions

File tree

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Added modeling for Python 3.15's `re.prefixmatch` and `re.Pattern.prefixmatch`, matching the existing support for their `match` counterparts.

‎python/ql/lib/semmle/python/frameworks/Stdlib.qll‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3105,12 +3105,15 @@ module StdlibPrivate {
31053105
*/
31063106
private class RegexExecutionMethod extends string {
31073107
RegexExecutionMethod() {
3108-
this in ["match", "fullmatch", "search", "split", "findall", "finditer", "sub", "subn"]
3108+
this in [
3109+
"match", "prefixmatch", "fullmatch", "search", "split", "findall", "finditer", "sub",
3110+
"subn"
3111+
]
31093112
}
31103113

31113114
/** Gets the index of the argument representing the string to be searched by a regex. */
31123115
int getStringArgIndex() {
3113-
this in ["match", "fullmatch", "search", "split", "findall", "finditer"] and
3116+
this in ["match", "prefixmatch", "fullmatch", "search", "split", "findall", "finditer"] and
31143117
result = 1
31153118
or
31163119
this in ["sub", "subn"] and
@@ -3244,7 +3247,7 @@ module StdlibPrivate {
32443247
this = "compiled re.Match"
32453248
)
32463249
|
3247-
result = re.getMember(["match", "search", "fullmatch"]).getACall()
3250+
result = re.getMember(["match", "prefixmatch", "search", "fullmatch"]).getACall()
32483251
)
32493252
}
32503253

‎python/ql/lib/semmle/python/regexp/internal/ParseRegExp.qll‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ private module FindRegexMode {
4444
or
4545
name = "search" and result = 2
4646
or
47-
name = "match" and result = 2
47+
name in ["match", "prefixmatch"] and result = 2
4848
or
4949
name = "split" and result = 3
5050
or

‎python/ql/lib/semmle/python/security/dataflow/ServerSideRequestForgeryCustomizations.qll‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -164,15 +164,15 @@ module ServerSideRequestForgery {
164164
["isalnum", "isalpha", "isdecimal", "isdigit", "isidentifier", "isnumeric", "isspace"])
165165
or
166166
branch = true and
167-
call = API::moduleImport("re").getMember(["match", "fullmatch"]).getACall() and
167+
call = API::moduleImport("re").getMember(["match", "prefixmatch", "fullmatch"]).getACall() and
168168
strNode = [call.getArg(1), call.getArgByName("string")]
169169
or
170170
branch = true and
171171
call =
172172
API::moduleImport("re")
173173
.getMember("compile")
174174
.getReturn()
175-
.getMember(["match", "fullmatch"])
175+
.getMember(["match", "prefixmatch", "fullmatch"])
176176
.getACall() and
177177
strNode = [call.getArg(0), call.getArgByName("string")]
178178
)
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import re
2+
from re import prefixmatch as prefix_match
3+
4+
ts = TAINTED_STRING
5+
pat = r"(?P<key>.*)"
6+
compiled_pat = re.compile(pat)
7+
8+
ensure_tainted(
9+
re.prefixmatch(pat, ts), # $ tainted
10+
re.prefixmatch(pattern=pat, string=ts), # $ tainted
11+
prefix_match(pat, ts), # $ tainted
12+
compiled_pat.prefixmatch(ts), # $ tainted
13+
compiled_pat.prefixmatch(string=ts, pos=0, endpos=10), # $ tainted
14+
15+
re.prefixmatch(pat, ts).string, # $ tainted
16+
re.prefixmatch(ts, "safe").re.pattern, # $ tainted
17+
compiled_pat.prefixmatch(ts).string, # $ tainted
18+
re.compile(ts).prefixmatch("safe").re.pattern, # $ tainted
19+
)
20+
21+
direct_match = re.prefixmatch(pat, ts)
22+
compiled_match = compiled_pat.prefixmatch(ts)
23+
ensure_tainted(
24+
direct_match.group(), # $ tainted
25+
direct_match.groups()[0], # $ tainted
26+
direct_match.groupdict()["key"], # $ tainted
27+
direct_match[0], # $ tainted
28+
direct_match["key"], # $ tainted
29+
direct_match.expand(r"\1"), # $ tainted
30+
31+
compiled_match.group(), # $ tainted
32+
compiled_match.groups()[0], # $ tainted
33+
compiled_match.groupdict()["key"], # $ tainted
34+
compiled_match[0], # $ tainted
35+
compiled_match["key"], # $ tainted
36+
compiled_match.expand(r"\1"), # $ tainted
37+
)
38+
39+
ensure_not_tainted(
40+
re.prefixmatch(pat, "safe").string,
41+
re.prefixmatch(pat, ts).re.pattern,
42+
re.prefixmatch(ts, "safe").group(),
43+
compiled_pat.prefixmatch("safe").string,
44+
compiled_pat.prefixmatch(ts).re.pattern,
45+
re.compile(ts).prefixmatch("safe").group(),
46+
)

‎python/ql/test/library-tests/regex/Mode.expected‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,8 @@
2626
| 63 | VERBOSE |
2727
| 65 | ASCII |
2828
| 77 | MULTILINE |
29+
| 91 | IGNORECASE |
30+
| 92 | DOTALL |
31+
| 92 | MULTILINE |
32+
| 93 | VERBOSE |
33+
| 94 | IGNORECASE |

‎python/ql/test/library-tests/regex/Regex.expected‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,8 @@
108108
| (?aimsx:a+) | qualified | 8 | 10 |
109109
| (?aimsx:a+) | sequence | 0 | 11 |
110110
| (?aimsx:a+) | sequence | 7 | 10 |
111+
| (?i) | empty group | 0 | 4 |
112+
| (?i) | sequence | 0 | 4 |
111113
| (?m)^(?!$) | $ | 8 | 9 |
112114
| (?m)^(?!$) | ^ | 4 | 5 |
113115
| (?m)^(?!$) | empty group | 0 | 4 |

‎python/ql/test/library-tests/regex/test.py‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,4 +85,10 @@
8585

8686
# Anchors
8787
re.compile(r'\Afoo\Z')
88-
re.compile(r'\bfoo\B')
88+
re.compile(r'\bfoo\B')
89+
90+
# Python 3.15 prefixmatch flags
91+
re.prefixmatch("", "", re.IGNORECASE)
92+
re.prefixmatch("", "", flags=re.DOTALL | re.MULTILINE)
93+
re.prefixmatch(pattern="", string="", flags=re.VERBOSE)
94+
re.prefixmatch("(?i)", "")

‎python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialBackTracking.expected‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,6 @@
33
| 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+$ |
44
| 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+$ |
55
| 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 |
6+
| 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+ |
7+
| 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+ |
8+
| 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+ |

‎python/ql/test/query-tests/Security/CWE-730-PolynomialReDoS/PolynomialReDoS.expected‎

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
| 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 |
55
| 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 |
66
| 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 |
7+
| 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 |
8+
| 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 |
9+
| 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 |
10+
| 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 |
711
edges
812
| test.py:2:26:2:32 | ControlFlowNode for ImportMember | test.py:2:26:2:32 | ControlFlowNode for request | provenance | |
913
| test.py:2:26:2:32 | ControlFlowNode for request | test.py:7:12:7:18 | ControlFlowNode for request | provenance | |
14+
| test.py:2:26:2:32 | ControlFlowNode for request | test.py:26:12:26:18 | ControlFlowNode for request | provenance | |
1015
| test.py:7:5:7:8 | ControlFlowNode for text | test.py:8:30:8:33 | ControlFlowNode for text | provenance | |
1116
| test.py:7:5:7:8 | ControlFlowNode for text | test.py:9:32:9:35 | ControlFlowNode for text | provenance | |
1217
| test.py:7:5:7:8 | ControlFlowNode for text | test.py:12:17:12:20 | ControlFlowNode for text | provenance | |
@@ -17,6 +22,13 @@ edges
1722
| test.py:7:12:7:35 | ControlFlowNode for Attribute() | test.py:7:5:7:8 | ControlFlowNode for text | provenance | |
1823
| test.py:14:33:14:39 | ControlFlowNode for my_text | test.py:16:24:16:30 | ControlFlowNode for my_text | provenance | |
1924
| test.py:18:28:18:31 | ControlFlowNode for text | test.py:14:33:14:39 | ControlFlowNode for my_text | provenance | |
25+
| test.py:26:5:26:8 | ControlFlowNode for text | test.py:27:38:27:41 | ControlFlowNode for text | provenance | |
26+
| test.py:26:5:26:8 | ControlFlowNode for text | test.py:28:53:28:56 | ControlFlowNode for text | provenance | |
27+
| test.py:26:5:26:8 | ControlFlowNode for text | test.py:31:25:31:28 | ControlFlowNode for text | provenance | |
28+
| test.py:26:5:26:8 | ControlFlowNode for text | test.py:32:32:32:35 | ControlFlowNode for text | provenance | |
29+
| test.py:26:12:26:18 | ControlFlowNode for request | test.py:26:12:26:23 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
30+
| test.py:26:12:26:23 | ControlFlowNode for Attribute | test.py:26:12:26:35 | ControlFlowNode for Attribute() | provenance | dict.get |
31+
| test.py:26:12:26:35 | ControlFlowNode for Attribute() | test.py:26:5:26:8 | ControlFlowNode for text | provenance | |
2032
nodes
2133
| test.py:2:26:2:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
2234
| test.py:2:26:2:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
@@ -31,4 +43,12 @@ nodes
3143
| test.py:16:24:16:30 | ControlFlowNode for my_text | semmle.label | ControlFlowNode for my_text |
3244
| test.py:18:28:18:31 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
3345
| test.py:21:18:21:21 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
46+
| test.py:26:5:26:8 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
47+
| test.py:26:12:26:18 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
48+
| test.py:26:12:26:23 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
49+
| test.py:26:12:26:35 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
50+
| test.py:27:38:27:41 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
51+
| test.py:28:53:28:56 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
52+
| test.py:31:25:31:28 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
53+
| test.py:32:32:32:35 | ControlFlowNode for text | semmle.label | ControlFlowNode for text |
3454
subpaths

0 commit comments

Comments
 (0)