Skip to content

Commit 8bac259

Browse files
owen-mcCopilot
andcommitted
Reject indirect URL parser callable sources
Do not infer exclusive parser origins for callable parameters or helper returns from API reachability. Add regressions covering both cases. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent f0e5329 commit 8bac259

3 files changed

Lines changed: 61 additions & 0 deletions

File tree

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,9 @@ private module Urllib {
9090
.getMember(["urlparse", "urlsplit"])
9191
.getACall() and
9292
forex(DataFlow::LocalSourceNode source | source = result.getFunction().getALocalSource() |
93+
// API reachability does not exclude other values passed to a parameter or returned by a call.
94+
not source instanceof DataFlow::ParameterNode and
95+
not source instanceof DataFlow::CallCfgNode and
9396
source =
9497
API::moduleImport("urllib")
9598
.getMember("parse")

‎python/ql/test/query-tests/Security/CWE-601-UrlRedirect/UrlRedirect.expected‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@
6565
| test.py:967:25:967:33 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:967:25:967:33 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
6666
| test.py:981:25:981:33 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:981:25:981:33 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
6767
| test.py:997:25:997:33 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:997:25:997:33 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
68+
| test.py:1010:25:1010:33 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:1010:25:1010:33 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
69+
| test.py:1029:25:1029:33 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:1029:25:1029:33 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
6870
edges
6971
| test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:1:26:1:32 | ControlFlowNode for request | provenance | |
7072
| test.py:1:26:1:32 | ControlFlowNode for request | test.py:7:14:7:20 | ControlFlowNode for request | provenance | |
@@ -133,6 +135,8 @@ edges
133135
| test.py:1:26:1:32 | ControlFlowNode for request | test.py:959:17:959:23 | ControlFlowNode for request | provenance | |
134136
| test.py:1:26:1:32 | ControlFlowNode for request | test.py:973:17:973:23 | ControlFlowNode for request | provenance | |
135137
| test.py:1:26:1:32 | ControlFlowNode for request | test.py:987:17:987:23 | ControlFlowNode for request | provenance | |
138+
| test.py:1:26:1:32 | ControlFlowNode for request | test.py:1002:17:1002:23 | ControlFlowNode for request | provenance | |
139+
| test.py:1:26:1:32 | ControlFlowNode for request | test.py:1025:17:1025:23 | ControlFlowNode for request | provenance | |
136140
| test.py:7:5:7:10 | ControlFlowNode for target | test.py:8:21:8:26 | ControlFlowNode for target | provenance | |
137141
| test.py:7:14:7:20 | ControlFlowNode for request | test.py:7:14:7:25 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
138142
| test.py:7:14:7:25 | ControlFlowNode for Attribute | test.py:7:14:7:43 | ControlFlowNode for Attribute() | provenance | dict.get |
@@ -466,6 +470,16 @@ edges
466470
| test.py:987:17:987:28 | ControlFlowNode for Attribute | test.py:987:17:987:46 | ControlFlowNode for Attribute() | provenance | dict.get |
467471
| test.py:987:17:987:46 | ControlFlowNode for Attribute() | test.py:987:17:987:65 | ControlFlowNode for Attribute() | provenance | Config |
468472
| test.py:987:17:987:65 | ControlFlowNode for Attribute() | test.py:987:5:987:13 | ControlFlowNode for untrusted | provenance | |
473+
| test.py:1002:5:1002:13 | ControlFlowNode for untrusted | test.py:1010:25:1010:33 | ControlFlowNode for untrusted | provenance | |
474+
| test.py:1002:17:1002:23 | ControlFlowNode for request | test.py:1002:17:1002:28 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
475+
| test.py:1002:17:1002:28 | ControlFlowNode for Attribute | test.py:1002:17:1002:46 | ControlFlowNode for Attribute() | provenance | dict.get |
476+
| test.py:1002:17:1002:46 | ControlFlowNode for Attribute() | test.py:1002:17:1002:65 | ControlFlowNode for Attribute() | provenance | Config |
477+
| test.py:1002:17:1002:65 | ControlFlowNode for Attribute() | test.py:1002:5:1002:13 | ControlFlowNode for untrusted | provenance | |
478+
| test.py:1025:5:1025:13 | ControlFlowNode for untrusted | test.py:1029:25:1029:33 | ControlFlowNode for untrusted | provenance | |
479+
| test.py:1025:17:1025:23 | ControlFlowNode for request | test.py:1025:17:1025:28 | ControlFlowNode for Attribute | provenance | AdditionalTaintStep |
480+
| test.py:1025:17:1025:28 | ControlFlowNode for Attribute | test.py:1025:17:1025:46 | ControlFlowNode for Attribute() | provenance | dict.get |
481+
| test.py:1025:17:1025:46 | ControlFlowNode for Attribute() | test.py:1025:17:1025:65 | ControlFlowNode for Attribute() | provenance | Config |
482+
| test.py:1025:17:1025:65 | ControlFlowNode for Attribute() | test.py:1025:5:1025:13 | ControlFlowNode for untrusted | provenance | |
469483
nodes
470484
| test.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
471485
| test.py:1:26:1:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
@@ -868,4 +882,16 @@ nodes
868882
| test.py:987:17:987:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
869883
| test.py:987:17:987:65 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
870884
| test.py:997:25:997:33 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted |
885+
| test.py:1002:5:1002:13 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted |
886+
| test.py:1002:17:1002:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
887+
| test.py:1002:17:1002:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
888+
| test.py:1002:17:1002:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
889+
| test.py:1002:17:1002:65 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
890+
| test.py:1010:25:1010:33 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted |
891+
| test.py:1025:5:1025:13 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted |
892+
| test.py:1025:17:1025:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
893+
| test.py:1025:17:1025:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
894+
| test.py:1025:17:1025:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
895+
| test.py:1025:17:1025:65 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
896+
| test.py:1029:25:1029:33 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted |
871897
subpaths

‎python/ql/test/query-tests/Security/CWE-601-UrlRedirect/test.py‎

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -996,3 +996,35 @@ def guarded_mutated_host_list():
996996
):
997997
return redirect(untrusted, code=302) # $ Alert
998998
return redirect("/", code=302)
999+
1000+
1001+
def redirect_with_parser(parse):
1002+
untrusted = request.args.get('target', '').replace("\\", "/").strip()
1003+
parts = parse(untrusted)
1004+
if (
1005+
untrusted.isprintable()
1006+
and not untrusted.startswith("///")
1007+
and not parts.scheme
1008+
and not parts.netloc
1009+
):
1010+
return redirect(untrusted, code=302) # $ Alert
1011+
return redirect("/", code=302)
1012+
1013+
1014+
@app.route('/mixed_parser_argument')
1015+
def mixed_parser_argument():
1016+
return redirect_with_parser(urlsplit if request.args.get('use-parser') else ignores_target)
1017+
1018+
1019+
def choose_parser():
1020+
return urlsplit if request.args.get('use-parser') else parses_request_target
1021+
1022+
1023+
@app.route('/mixed_parser_return')
1024+
def mixed_parser_return():
1025+
untrusted = request.args.get('target', '').replace("\\", "/")
1026+
parse = choose_parser()
1027+
allowed_host = parse("https://example.com").hostname
1028+
if urlsplit(untrusted).netloc == allowed_host:
1029+
return redirect(untrusted, code=302) # $ Alert
1030+
return redirect("/", code=302)

0 commit comments

Comments
 (0)