diff --git a/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll b/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll index b4843f1a8eb..1476d0381ea 100644 --- a/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll +++ b/python/ql/lib/semmle/python/security/dataflow/UrlRedirectCustomizations.qll @@ -70,4 +70,31 @@ module UrlRedirect { * A comparison with a constant string, considered as a sanitizer-guard. */ class StringConstCompareAsSanitizerGuard extends Sanitizer, StringConstCompareBarrier { } + + private import semmle.python.ApiGraphs + + private predicate djangoUrlHasAllowedHostAndScheme( + DataFlow::GuardNode g, ControlFlowNode node, boolean branch + ) { + exists(API::CallNode call | + call = + API::moduleImport("django") + .getMember("utils") + .getMember("http") + .getMember("url_has_allowed_host_and_scheme") + .getACall() and + g = call.asCfgNode() and + node = call.getParameter(0, "url").asSink().asCfgNode() and + branch = true + ) + } + + /** + * A call to `django.utils.http.url_has_allowed_host_and_scheme`, considered as a sanitizer-guard. + */ + private class DjangoAllowedUrl extends Sanitizer { + DjangoAllowedUrl() { + this = DataFlow::BarrierGuard::getABarrierNode() + } + } } diff --git a/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/UrlRedirect.expected b/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/UrlRedirect.expected index 21f498c5d94..ea47807a211 100644 --- a/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/UrlRedirect.expected +++ b/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/UrlRedirect.expected @@ -49,7 +49,6 @@ edges | test.py:81:17:81:46 | ControlFlowNode for Attribute() | test.py:81:5:81:13 | SSA variable untrusted | | test.py:82:5:82:10 | SSA variable unsafe | test.py:83:21:83:26 | ControlFlowNode for unsafe | | test.py:90:5:90:13 | SSA variable untrusted | test.py:93:18:93:26 | ControlFlowNode for untrusted | -| test.py:90:5:90:13 | SSA variable untrusted | test.py:95:25:95:33 | ControlFlowNode for untrusted | | test.py:90:17:90:23 | ControlFlowNode for request | test.py:90:17:90:28 | ControlFlowNode for Attribute | | test.py:90:17:90:28 | ControlFlowNode for Attribute | test.py:90:17:90:46 | ControlFlowNode for Attribute() | | test.py:90:17:90:46 | ControlFlowNode for Attribute() | test.py:90:5:90:13 | SSA variable untrusted | @@ -108,7 +107,6 @@ nodes | test.py:90:17:90:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute | | test.py:90:17:90:46 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() | | test.py:93:18:93:26 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted | -| test.py:95:25:95:33 | ControlFlowNode for untrusted | semmle.label | ControlFlowNode for untrusted | subpaths #select | 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 | @@ -120,4 +118,3 @@ subpaths | 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 | | 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 | | 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 | -| test.py:95:25:95:33 | ControlFlowNode for untrusted | test.py:1:26:1:32 | ControlFlowNode for ImportMember | test.py:95:25:95:33 | ControlFlowNode for untrusted | Untrusted URL redirection depends on a $@. | test.py:1:26:1:32 | ControlFlowNode for ImportMember | user-provided value | diff --git a/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/test.py b/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/test.py index 419ae428edd..0a02b4ac1d5 100644 --- a/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/test.py +++ b/python/ql/test/query-tests/Security/CWE-601-UrlRedirect/test.py @@ -92,6 +92,6 @@ def ok6(): if math.random() > 0.5: redirect(untrusted, code=302) # NOT OK if url_has_allowed_host_and_scheme(untrusted, allowed_hosts=None): - return redirect(untrusted, code=302) # OK - but is flagged! + return redirect(untrusted, code=302) # OK return redirect("https://example.com", code=302) # OK \ No newline at end of file