Skip to content

Commit f441c68

Browse files
Merge pull request github#16657 from joefarebrother/python-partial-ssrf-fp
Python: Add additional sanitizers to SSRF
2 parents e9bd85e + 93f10fc commit f441c68

File tree

3 files changed

+90
-1
lines changed

3 files changed

+90
-1
lines changed

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ private import semmle.python.dataflow.new.DataFlow
99
private import semmle.python.Concepts
1010
private import semmle.python.dataflow.new.RemoteFlowSources
1111
private import semmle.python.dataflow.new.BarrierGuards
12+
private import semmle.python.ApiGraphs
1213

1314
/**
1415
* Provides default sources, sinks and sanitizers for detecting
@@ -137,4 +138,34 @@ module ServerSideRequestForgery {
137138
)
138139
}
139140
}
141+
142+
/** A validation that a string does not contain certain characters, considered as a sanitizer. */
143+
private class StringRestrictionSanitizerGuard extends Sanitizer {
144+
StringRestrictionSanitizerGuard() {
145+
this = DataFlow::BarrierGuard<stringRestriction/3>::getABarrierNode()
146+
}
147+
}
148+
149+
private predicate stringRestriction(DataFlow::GuardNode g, ControlFlowNode node, boolean branch) {
150+
exists(DataFlow::MethodCallNode call, DataFlow::Node strNode |
151+
call.asCfgNode() = g and strNode.asCfgNode() = node
152+
|
153+
branch = true and
154+
call.calls(strNode,
155+
["isalnum", "isalpha", "isdecimal", "isdigit", "isidentifier", "isnumeric", "isspace"])
156+
or
157+
branch = true and
158+
call = API::moduleImport("re").getMember(["match", "fullmatch"]).getACall() and
159+
strNode = [call.getArg(1), call.getArgByName("string")]
160+
or
161+
branch = true and
162+
call =
163+
API::moduleImport("re")
164+
.getMember("compile")
165+
.getReturn()
166+
.getMember(["match", "fullmatch"])
167+
.getACall() and
168+
strNode = [call.getArg(0), call.getArgByName("string")]
169+
)
170+
}
140171
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Additional sanitizers have been added to the `py/full-ssrf` and `py/partial-ssrf` queries for methods that verify a string contains only a certain set of characters, such as `.isalnum()` as well as regular expression tests.

python/ql/test/query-tests/Security/CWE-918-ServerSideRequestForgery/full_partial_test.py

Lines changed: 55 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from flask import request
22

33
import requests
4-
4+
import re
55

66
def full_ssrf():
77
user_input = request.args['untrusted_input']
@@ -120,3 +120,57 @@ def partial_ssrf_6():
120120

121121
url = f"https://example.com/foo#{user_input}"
122122
requests.get(url) # NOT OK -- user only controlled fragment
123+
124+
def partial_ssrf_7():
125+
user_input = request.args['untrusted_input']
126+
127+
if user_input.isalnum():
128+
url = f"https://example.com/foo#{user_input}"
129+
requests.get(url) # OK - user input can only contain alphanumerical characters
130+
131+
if user_input.isalpha():
132+
url = f"https://example.com/foo#{user_input}"
133+
requests.get(url) # OK - user input can only contain alphabetical characters
134+
135+
if user_input.isdecimal():
136+
url = f"https://example.com/foo#{user_input}"
137+
requests.get(url) # OK - user input can only contain decimal characters
138+
139+
if user_input.isdigit():
140+
url = f"https://example.com/foo#{user_input}"
141+
requests.get(url) # OK - user input can only contain digits
142+
143+
if user_input.isnumeric():
144+
url = f"https://example.com/foo#{user_input}"
145+
requests.get(url) # OK - user input can only contain numeric characters
146+
147+
if user_input.isspace():
148+
url = f"https://example.com/foo#{user_input}"
149+
requests.get(url) # OK - user input can only contain whitespace characters
150+
151+
if re.fullmatch(r'[a-zA-Z0-9]+', user_input):
152+
url = f"https://example.com/foo#{user_input}"
153+
requests.get(url) # OK - user input can only contain alphanumerical characters
154+
155+
if re.fullmatch(r'.*[a-zA-Z0-9]+.*', user_input):
156+
url = f"https://example.com/foo#{user_input}"
157+
requests.get(url) # NOT OK, but NOT FOUND - user input can contain arbitrary characters
158+
159+
160+
if re.match(r'^[a-zA-Z0-9]+$', user_input):
161+
url = f"https://example.com/foo#{user_input}"
162+
requests.get(url) # OK - user input can only contain alphanumerical characters
163+
164+
if re.match(r'[a-zA-Z0-9]+', user_input):
165+
url = f"https://example.com/foo#{user_input}"
166+
requests.get(url) # NOT OK, but NOT FOUND - user input can contain arbitrary character as a suffix.
167+
168+
reg = re.compile(r'^[a-zA-Z0-9]+$')
169+
170+
if reg.match(user_input):
171+
url = f"https://example.com/foo#{user_input}"
172+
requests.get(url) # OK - user input can only contain alphanumerical characters
173+
174+
if reg.fullmatch(user_input):
175+
url = f"https://example.com/foo#{user_input}"
176+
requests.get(url) # OK - user input can only contain alphanumerical characters

0 commit comments

Comments
 (0)