Skip to content

Commit f1a0ec2

Browse files
authored
Merge pull request #4981 from RasmusWL/port-url-redirect-query
Python: Port url redirect query
2 parents 9af99f1 + ddd362b commit f1a0ec2

File tree

16 files changed

+462
-47
lines changed

16 files changed

+462
-47
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Ported URL redirection (`py/url-redirection`) query to use new data-flow library. This might result in different results, but overall a more robust and accurate analysis.

python/ql/src/Security/CWE-601/UrlRedirect.ql

Lines changed: 6 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -12,30 +12,10 @@
1212
*/
1313

1414
import python
15-
import semmle.python.security.Paths
16-
import semmle.python.web.HttpRedirect
17-
import semmle.python.web.HttpRequest
18-
import semmle.python.security.strings.Untrusted
15+
import semmle.python.security.dataflow.UrlRedirect
16+
import DataFlow::PathGraph
1917

20-
/** Url redirection is a problem only if the user controls the prefix of the URL */
21-
class UntrustedPrefixStringKind extends UntrustedStringKind {
22-
override TaintKind getTaintForFlowStep(ControlFlowNode fromnode, ControlFlowNode tonode) {
23-
result = UntrustedStringKind.super.getTaintForFlowStep(fromnode, tonode) and
24-
not tonode.(BinaryExprNode).getRight() = fromnode
25-
}
26-
}
27-
28-
class UrlRedirectConfiguration extends TaintTracking::Configuration {
29-
UrlRedirectConfiguration() { this = "URL redirect configuration" }
30-
31-
override predicate isSource(TaintTracking::Source source) {
32-
source instanceof HttpRequestTaintSource
33-
}
34-
35-
override predicate isSink(TaintTracking::Sink sink) { sink instanceof HttpRedirectTaintSink }
36-
}
37-
38-
from UrlRedirectConfiguration config, TaintedPathSource src, TaintedPathSink sink
39-
where config.hasFlowPath(src, sink)
40-
select sink.getSink(), src, sink, "Untrusted URL redirection due to $@.", src.getSource(),
41-
"a user-provided value"
18+
from UrlRedirectConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where config.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink, "Untrusted URL redirection due to $@.", source.getNode(),
21+
"A user-provided value"
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/**
2+
* @name URL redirection from remote source
3+
* @description URL redirection based on unvalidated user input
4+
* may cause redirection to malicious web sites.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @sub-severity low
8+
* @id py/url-redirection
9+
* @tags security
10+
* external/cwe/cwe-601
11+
* @precision high
12+
*/
13+
14+
import python
15+
import semmle.python.security.Paths
16+
import semmle.python.web.HttpRedirect
17+
import semmle.python.web.HttpRequest
18+
import semmle.python.security.strings.Untrusted
19+
20+
/** Url redirection is a problem only if the user controls the prefix of the URL */
21+
class UntrustedPrefixStringKind extends UntrustedStringKind {
22+
override TaintKind getTaintForFlowStep(ControlFlowNode fromnode, ControlFlowNode tonode) {
23+
result = UntrustedStringKind.super.getTaintForFlowStep(fromnode, tonode) and
24+
not tonode.(BinaryExprNode).getRight() = fromnode
25+
}
26+
}
27+
28+
class UrlRedirectConfiguration extends TaintTracking::Configuration {
29+
UrlRedirectConfiguration() { this = "URL redirect configuration" }
30+
31+
override predicate isSource(TaintTracking::Source source) {
32+
source instanceof HttpRequestTaintSource
33+
}
34+
35+
override predicate isSink(TaintTracking::Sink sink) { sink instanceof HttpRedirectTaintSink }
36+
}
37+
38+
from UrlRedirectConfiguration config, TaintedPathSource src, TaintedPathSink sink
39+
where config.hasFlowPath(src, sink)
40+
select sink.getSink(), src, sink, "Untrusted URL redirection due to $@.", src.getSource(),
41+
"a user-provided value"

python/ql/src/semmle/python/Concepts.qll

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -473,5 +473,40 @@ module HTTP {
473473
}
474474
}
475475
}
476+
477+
/**
478+
* A data-flow node that creates a HTTP redirect response on a server.
479+
*
480+
* Note: we don't require that this redirect must be sent to a client (a kind of
481+
* "if a tree falls in a forest and nobody hears it" situation).
482+
*
483+
* Extend this class to refine existing API models. If you want to model new APIs,
484+
* extend `HttpRedirectResponse::Range` instead.
485+
*/
486+
class HttpRedirectResponse extends HttpResponse {
487+
override HttpRedirectResponse::Range range;
488+
489+
HttpRedirectResponse() { this = range }
490+
491+
/** Gets the data-flow node that specifies the location of this HTTP redirect response. */
492+
DataFlow::Node getRedirectLocation() { result = range.getRedirectLocation() }
493+
}
494+
495+
/** Provides a class for modeling new HTTP redirect response APIs. */
496+
module HttpRedirectResponse {
497+
/**
498+
* A data-flow node that creates a HTTP redirect response on a server.
499+
*
500+
* Note: we don't require that this redirect must be sent to a client (a kind of
501+
* "if a tree falls in a forest and nobody hears it" situation).
502+
*
503+
* Extend this class to model new APIs. If you want to refine existing API models,
504+
* extend `HttpResponse` instead.
505+
*/
506+
abstract class Range extends HTTP::Server::HttpResponse::Range {
507+
/** Gets the data-flow node that specifies the location of this HTTP redirect response. */
508+
abstract DataFlow::Node getRedirectLocation();
509+
}
510+
}
476511
}
477512
}

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

Lines changed: 104 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ private module Django {
3535
* WARNING: Only holds for a few predefined attributes.
3636
*/
3737
private DataFlow::Node django_attr(DataFlow::TypeTracker t, string attr_name) {
38-
attr_name in ["db", "urls", "http", "conf", "views"] and
38+
attr_name in ["db", "urls", "http", "conf", "views", "shortcuts"] and
3939
(
4040
t.start() and
4141
result = DataFlow::importNode("django" + "." + attr_name)
@@ -724,7 +724,8 @@ private module Django {
724724
*
725725
* Use the predicate `HttpResponseRedirect::instance()` to get references to instances of `django.http.response.HttpResponseRedirect`.
726726
*/
727-
abstract class InstanceSource extends HttpResponse::InstanceSource, DataFlow::Node { }
727+
abstract class InstanceSource extends HttpResponse::InstanceSource,
728+
HTTP::Server::HttpRedirectResponse::Range, DataFlow::Node { }
728729

729730
/** A direct instantiation of `django.http.response.HttpResponseRedirect`. */
730731
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
@@ -739,6 +740,10 @@ private module Django {
739740
result.asCfgNode() in [node.getArg(1), node.getArgByName("content")]
740741
}
741742

743+
override DataFlow::Node getRedirectLocation() {
744+
result.asCfgNode() in [node.getArg(0), node.getArgByName("redirect_to")]
745+
}
746+
742747
// How to support the `headers` argument here?
743748
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
744749

@@ -790,7 +795,8 @@ private module Django {
790795
*
791796
* Use the predicate `HttpResponsePermanentRedirect::instance()` to get references to instances of `django.http.response.HttpResponsePermanentRedirect`.
792797
*/
793-
abstract class InstanceSource extends HttpResponse::InstanceSource, DataFlow::Node { }
798+
abstract class InstanceSource extends HttpResponse::InstanceSource,
799+
HTTP::Server::HttpRedirectResponse::Range, DataFlow::Node { }
794800

795801
/** A direct instantiation of `django.http.response.HttpResponsePermanentRedirect`. */
796802
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
@@ -805,6 +811,10 @@ private module Django {
805811
result.asCfgNode() in [node.getArg(1), node.getArgByName("content")]
806812
}
807813

814+
override DataFlow::Node getRedirectLocation() {
815+
result.asCfgNode() in [node.getArg(0), node.getArgByName("redirect_to")]
816+
}
817+
808818
// How to support the `headers` argument here?
809819
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
810820

@@ -1907,6 +1917,62 @@ private module Django {
19071917
}
19081918
}
19091919
}
1920+
1921+
// -------------------------------------------------------------------------
1922+
// django.shortcuts
1923+
// -------------------------------------------------------------------------
1924+
/** Gets a reference to the `django.shortcuts` module. */
1925+
DataFlow::Node shortcuts() { result = django_attr("shortcuts") }
1926+
1927+
/** Provides models for the `django.shortcuts` module */
1928+
module shortcuts {
1929+
/**
1930+
* Gets a reference to the attribute `attr_name` of the `django.shortcuts` module.
1931+
* WARNING: Only holds for a few predefined attributes.
1932+
*/
1933+
private DataFlow::Node shortcuts_attr(DataFlow::TypeTracker t, string attr_name) {
1934+
attr_name in ["redirect"] and
1935+
(
1936+
t.start() and
1937+
result = DataFlow::importNode("django.shortcuts" + "." + attr_name)
1938+
or
1939+
t.startInAttr(attr_name) and
1940+
result = shortcuts()
1941+
)
1942+
or
1943+
// Due to bad performance when using normal setup with `shortcuts_attr(t2, attr_name).track(t2, t)`
1944+
// we have inlined that code and forced a join
1945+
exists(DataFlow::TypeTracker t2 |
1946+
exists(DataFlow::StepSummary summary |
1947+
shortcuts_attr_first_join(t2, attr_name, result, summary) and
1948+
t = t2.append(summary)
1949+
)
1950+
)
1951+
}
1952+
1953+
pragma[nomagic]
1954+
private predicate shortcuts_attr_first_join(
1955+
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
1956+
DataFlow::StepSummary summary
1957+
) {
1958+
DataFlow::StepSummary::step(shortcuts_attr(t2, attr_name), res, summary)
1959+
}
1960+
1961+
/**
1962+
* Gets a reference to the attribute `attr_name` of the `django.shortcuts` module.
1963+
* WARNING: Only holds for a few predefined attributes.
1964+
*/
1965+
private DataFlow::Node shortcuts_attr(string attr_name) {
1966+
result = shortcuts_attr(DataFlow::TypeTracker::end(), attr_name)
1967+
}
1968+
1969+
/**
1970+
* Gets a reference to the `django.shortcuts.redirect` function
1971+
*
1972+
* See https://docs.djangoproject.com/en/3.1/topics/http/shortcuts/#redirect
1973+
*/
1974+
DataFlow::Node redirect() { result = shortcuts_attr("redirect") }
1975+
}
19101976
}
19111977

19121978
// ---------------------------------------------------------------------------
@@ -2230,4 +2296,39 @@ private module Django {
22302296
)
22312297
}
22322298
}
2299+
2300+
// ---------------------------------------------------------------------------
2301+
// django.shortcuts.redirect
2302+
// ---------------------------------------------------------------------------
2303+
/**
2304+
* A call to `django.shortcuts.redirect`.
2305+
*
2306+
* Note: This works differently depending on what argument is used.
2307+
* _One_ option is to redirect to a full URL.
2308+
*
2309+
* See https://docs.djangoproject.com/en/3.1/topics/http/shortcuts/#redirect
2310+
*/
2311+
private class DjangoShortcutsRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
2312+
DataFlow::CfgNode {
2313+
override CallNode node;
2314+
2315+
DjangoShortcutsRedirectCall() { node.getFunction() = django::shortcuts::redirect().asCfgNode() }
2316+
2317+
/**
2318+
* Gets the data-flow node that specifies the location of this HTTP redirect response.
2319+
*
2320+
* Note: For `django.shortcuts.redirect`, the result might not be a full URL
2321+
* (as usually expected by this method), but could be a relative URL,
2322+
* a string identifying a view, or a Django model.
2323+
*/
2324+
override DataFlow::Node getRedirectLocation() {
2325+
result.asCfgNode() in [node.getArg(0), node.getArgByName("to")]
2326+
}
2327+
2328+
override DataFlow::Node getBody() { none() }
2329+
2330+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
2331+
2332+
override string getMimetypeDefault() { none() }
2333+
}
22332334
}

python/ql/src/semmle/python/frameworks/Flask.qll

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ private module FlaskModel {
3434
* WARNING: Only holds for a few predefined attributes.
3535
*/
3636
private DataFlow::Node flask_attr(DataFlow::TypeTracker t, string attr_name) {
37-
attr_name in ["request", "make_response", "Response", "views"] and
37+
attr_name in ["request", "make_response", "Response", "views", "redirect"] and
3838
(
3939
t.start() and
4040
result = DataFlow::importNode("flask" + "." + attr_name)
@@ -669,4 +669,31 @@ private module FlaskModel {
669669

670670
override string getMimetypeDefault() { result = "text/html" }
671671
}
672+
673+
/**
674+
* A call to the `flask.redirect` function.
675+
*
676+
* See https://flask.palletsprojects.com/en/1.1.x/api/#flask.redirect
677+
*/
678+
private class FlaskRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
679+
DataFlow::CfgNode {
680+
override CallNode node;
681+
682+
FlaskRedirectCall() { node.getFunction() = flask_attr("redirect").asCfgNode() }
683+
684+
override DataFlow::Node getRedirectLocation() {
685+
result.asCfgNode() in [node.getArg(0), node.getArgByName("location")]
686+
}
687+
688+
override DataFlow::Node getBody() { none() }
689+
690+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
691+
692+
override string getMimetypeDefault() {
693+
// note that while you're not able to set content yourself, the function will
694+
// actually fill out some default content, that is served with mimetype
695+
// `text/html`.
696+
result = "text/html"
697+
}
698+
}
672699
}

python/ql/src/semmle/python/frameworks/Tornado.qll

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,17 @@ private module Tornado {
216216
/** Gets a reference to one of the methods `get_arguments`, `get_body_arguments`, `get_query_arguments`. */
217217
DataFlow::Node argumentsMethod() { result = argumentsMethod(DataFlow::TypeTracker::end()) }
218218

219+
/** Gets a reference the `redirect` method. */
220+
private DataFlow::Node redirectMethod(DataFlow::TypeTracker t) {
221+
t.startInAttr("redirect") and
222+
result = instance()
223+
or
224+
exists(DataFlow::TypeTracker t2 | result = redirectMethod(t2).track(t2, t))
225+
}
226+
227+
/** Gets a reference the `redirect` method. */
228+
DataFlow::Node redirectMethod() { result = redirectMethod(DataFlow::TypeTracker::end()) }
229+
219230
/** Gets a reference to the `write` method. */
220231
private DataFlow::Node writeMethod(DataFlow::TypeTracker t) {
221232
t.startInAttr("write") and
@@ -556,7 +567,31 @@ private module Tornado {
556567
// Response modeling
557568
// ---------------------------------------------------------------------------
558569
/**
559-
* A call to `tornado.web.RequestHandler.write` method.
570+
* A call to the `tornado.web.RequestHandler.redirect` method.
571+
*
572+
* See https://www.tornadoweb.org/en/stable/web.html?highlight=write#tornado.web.RequestHandler.redirect
573+
*/
574+
private class TornadoRequestHandlerRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
575+
DataFlow::CfgNode {
576+
override CallNode node;
577+
578+
TornadoRequestHandlerRedirectCall() {
579+
node.getFunction() = tornado::web::RequestHandler::redirectMethod().asCfgNode()
580+
}
581+
582+
override DataFlow::Node getRedirectLocation() {
583+
result.asCfgNode() in [node.getArg(0), node.getArgByName("url")]
584+
}
585+
586+
override DataFlow::Node getBody() { none() }
587+
588+
override string getMimetypeDefault() { none() }
589+
590+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
591+
}
592+
593+
/**
594+
* A call to the `tornado.web.RequestHandler.write` method.
560595
*
561596
* See https://www.tornadoweb.org/en/stable/web.html?highlight=write#tornado.web.RequestHandler.write
562597
*/

0 commit comments

Comments
 (0)