Python: Model `prefixmatch` regular expression APIs · owen-mc/codeql@000be49 · GitHub
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

‎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

0 commit comments

Comments
 (0)