Skip to content

Commit cd59737

Browse files
authored
Merge pull request #14112 from erik-krogh/pyAllowedHosts
Py: add sanitizer guard for `url_has_allowed_host_and_scheme`
2 parents 7400b47 + 7292730 commit cd59737

File tree

4 files changed

+57
-0
lines changed

4 files changed

+57
-0
lines changed

python/ql/lib/semmle/python/frameworks/Django.qll

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ private import semmle.python.regex
1515
private import semmle.python.frameworks.internal.PoorMansFunctionResolution
1616
private import semmle.python.frameworks.internal.SelfRefMixin
1717
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
18+
private import semmle.python.security.dataflow.UrlRedirectCustomizations
1819

1920
/**
2021
* INTERNAL: Do not use.
@@ -2788,4 +2789,31 @@ module PrivateDjango {
27882789

27892790
override predicate csrfEnabled() { decoratorName in ["csrf_protect", "requires_csrf_token"] }
27902791
}
2792+
2793+
private predicate djangoUrlHasAllowedHostAndScheme(
2794+
DataFlow::GuardNode g, ControlFlowNode node, boolean branch
2795+
) {
2796+
exists(API::CallNode call |
2797+
call =
2798+
API::moduleImport("django")
2799+
.getMember("utils")
2800+
.getMember("http")
2801+
.getMember("url_has_allowed_host_and_scheme")
2802+
.getACall() and
2803+
g = call.asCfgNode() and
2804+
node = call.getParameter(0, "url").asSink().asCfgNode() and
2805+
branch = true
2806+
)
2807+
}
2808+
2809+
/**
2810+
* A call to `django.utils.http.url_has_allowed_host_and_scheme`, considered as a sanitizer-guard for URL redirection.
2811+
*
2812+
* See https://docs.djangoproject.com/en/4.2/_modules/django/utils/http/
2813+
*/
2814+
private class DjangoAllowedUrl extends UrlRedirect::Sanitizer {
2815+
DjangoAllowedUrl() {
2816+
this = DataFlow::BarrierGuard<djangoUrlHasAllowedHostAndScheme/3>::getABarrierNode()
2817+
}
2818+
}
27912819
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: minorAnalysis
3+
---
4+
* Improved _URL redirection from remote source_ (`py/url-redirection`) query to not alert when URL has been checked with `django.utils.http. url_has_allowed_host_and_scheme`.

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

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ edges
88
| test.py:1:26:1:32 | GSSA Variable request | test.py:67:17:67:23 | ControlFlowNode for request |
99
| test.py:1:26:1:32 | GSSA Variable request | test.py:74:17:74:23 | ControlFlowNode for request |
1010
| test.py:1:26:1:32 | GSSA Variable request | test.py:81:17:81:23 | ControlFlowNode for request |
11+
| test.py:1:26:1:32 | GSSA Variable request | test.py:90:17:90:23 | ControlFlowNode for request |
1112
| test.py:7:5:7:10 | SSA variable target | test.py:8:21:8:26 | ControlFlowNode for target |
1213
| test.py:7:14:7:20 | ControlFlowNode for request | test.py:7:14:7:25 | ControlFlowNode for Attribute |
1314
| test.py:7:14:7:25 | ControlFlowNode for Attribute | test.py:7:14:7:43 | ControlFlowNode for Attribute() |
@@ -47,6 +48,10 @@ edges
4748
| test.py:81:17:81:28 | ControlFlowNode for Attribute | test.py:81:17:81:46 | ControlFlowNode for Attribute() |
4849
| test.py:81:17:81:46 | ControlFlowNode for Attribute() | test.py:81:5:81:13 | SSA variable untrusted |
4950
| test.py:82:5:82:10 | SSA variable unsafe | test.py:83:21:83:26 | ControlFlowNode for unsafe |
51+
| test.py:90:5:90:13 | SSA variable untrusted | test.py:93:18:93:26 | ControlFlowNode for untrusted |
52+
| test.py:90:17:90:23 | ControlFlowNode for request | test.py:90:17:90:28 | ControlFlowNode for Attribute |
53+
| test.py:90:17:90:28 | ControlFlowNode for Attribute | test.py:90:17:90:46 | ControlFlowNode for Attribute() |
54+
| test.py:90:17:90:46 | ControlFlowNode for Attribute() | test.py:90:5:90:13 | SSA variable untrusted |
5055
nodes
5156
| test.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
5257
| test.py:1:26:1:32 | GSSA Variable request | semmle.label | GSSA Variable request |
@@ -97,6 +102,11 @@ nodes
97102
| test.py:81:17:81:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
98103
| test.py:82:5:82:10 | SSA variable unsafe | semmle.label | SSA variable unsafe |
99104
| test.py:83:21:83:26 | ControlFlowNode for unsafe | semmle.label | ControlFlowNode for unsafe |
105+
| test.py:90:5:90:13 | SSA variable untrusted | semmle.label | SSA variable untrusted |
106+
| test.py:90:17:90:23 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
107+
| test.py:90:17:90:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
108+
| test.py:90:17:90:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
109+
| test.py:93:18:93:26 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted |
100110
subpaths
101111
#select
102112
| test.py:8:21:8:26 | ControlFlowNode for target | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:8:21:8:26 | ControlFlowNode for target | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
@@ -107,3 +117,4 @@ subpaths
107117
| test.py:69:21:69:26 | ControlFlowNode for unsafe | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:69:21:69:26 | ControlFlowNode for unsafe | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
108118
| test.py:76:21:76:26 | ControlFlowNode for unsafe | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:76:21:76:26 | ControlFlowNode for unsafe | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
109119
| test.py:83:21:83:26 | ControlFlowNode for unsafe | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:83:21:83:26 | ControlFlowNode for unsafe | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |
120+
| test.py:93:18:93:26 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:93:18:93:26 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value |

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,17 @@ def not_ok4():
8181
untrusted = request.args.get('target', '')
8282
unsafe = "%s?login=success" % untrusted
8383
return redirect(unsafe, code=302)
84+
85+
from django.utils.http import url_has_allowed_host_and_scheme
86+
import math
87+
88+
@app.route('/ok6')
89+
def ok6():
90+
untrusted = request.args.get('target', '')
91+
# random chance.
92+
if math.random() > 0.5:
93+
redirect(untrusted, code=302) # NOT OK
94+
if url_has_allowed_host_and_scheme(untrusted, allowed_hosts=None):
95+
return redirect(untrusted, code=302) # OK
96+
97+
return redirect("https://example.com", code=302) # OK

0 commit comments

Comments
 (0)