Merge pull request #4981 from RasmusWL/port-url-redirect-query

Python: Port url redirect query
This commit is contained in:
yoff
2021-02-06 00:39:10 +01:00
committed by GitHub
16 changed files with 462 additions and 47 deletions

View File

@@ -0,0 +1,2 @@
lgtm,codescanning
* 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.

View File

@@ -12,30 +12,10 @@
*/
import python
import semmle.python.security.Paths
import semmle.python.web.HttpRedirect
import semmle.python.web.HttpRequest
import semmle.python.security.strings.Untrusted
import semmle.python.security.dataflow.UrlRedirect
import DataFlow::PathGraph
/** Url redirection is a problem only if the user controls the prefix of the URL */
class UntrustedPrefixStringKind extends UntrustedStringKind {
override TaintKind getTaintForFlowStep(ControlFlowNode fromnode, ControlFlowNode tonode) {
result = UntrustedStringKind.super.getTaintForFlowStep(fromnode, tonode) and
not tonode.(BinaryExprNode).getRight() = fromnode
}
}
class UrlRedirectConfiguration extends TaintTracking::Configuration {
UrlRedirectConfiguration() { this = "URL redirect configuration" }
override predicate isSource(TaintTracking::Source source) {
source instanceof HttpRequestTaintSource
}
override predicate isSink(TaintTracking::Sink sink) { sink instanceof HttpRedirectTaintSink }
}
from UrlRedirectConfiguration config, TaintedPathSource src, TaintedPathSink sink
where config.hasFlowPath(src, sink)
select sink.getSink(), src, sink, "Untrusted URL redirection due to $@.", src.getSource(),
"a user-provided value"
from UrlRedirectConfiguration config, DataFlow::PathNode source, DataFlow::PathNode sink
where config.hasFlowPath(source, sink)
select sink.getNode(), source, sink, "Untrusted URL redirection due to $@.", source.getNode(),
"A user-provided value"

View File

@@ -0,0 +1,41 @@
/**
* @name URL redirection from remote source
* @description URL redirection based on unvalidated user input
* may cause redirection to malicious web sites.
* @kind path-problem
* @problem.severity error
* @sub-severity low
* @id py/url-redirection
* @tags security
* external/cwe/cwe-601
* @precision high
*/
import python
import semmle.python.security.Paths
import semmle.python.web.HttpRedirect
import semmle.python.web.HttpRequest
import semmle.python.security.strings.Untrusted
/** Url redirection is a problem only if the user controls the prefix of the URL */
class UntrustedPrefixStringKind extends UntrustedStringKind {
override TaintKind getTaintForFlowStep(ControlFlowNode fromnode, ControlFlowNode tonode) {
result = UntrustedStringKind.super.getTaintForFlowStep(fromnode, tonode) and
not tonode.(BinaryExprNode).getRight() = fromnode
}
}
class UrlRedirectConfiguration extends TaintTracking::Configuration {
UrlRedirectConfiguration() { this = "URL redirect configuration" }
override predicate isSource(TaintTracking::Source source) {
source instanceof HttpRequestTaintSource
}
override predicate isSink(TaintTracking::Sink sink) { sink instanceof HttpRedirectTaintSink }
}
from UrlRedirectConfiguration config, TaintedPathSource src, TaintedPathSink sink
where config.hasFlowPath(src, sink)
select sink.getSink(), src, sink, "Untrusted URL redirection due to $@.", src.getSource(),
"a user-provided value"

View File

@@ -473,5 +473,40 @@ module HTTP {
}
}
}
/**
* A data-flow node that creates a HTTP redirect response on a server.
*
* Note: we don't require that this redirect must be sent to a client (a kind of
* "if a tree falls in a forest and nobody hears it" situation).
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `HttpRedirectResponse::Range` instead.
*/
class HttpRedirectResponse extends HttpResponse {
override HttpRedirectResponse::Range range;
HttpRedirectResponse() { this = range }
/** Gets the data-flow node that specifies the location of this HTTP redirect response. */
DataFlow::Node getRedirectLocation() { result = range.getRedirectLocation() }
}
/** Provides a class for modeling new HTTP redirect response APIs. */
module HttpRedirectResponse {
/**
* A data-flow node that creates a HTTP redirect response on a server.
*
* Note: we don't require that this redirect must be sent to a client (a kind of
* "if a tree falls in a forest and nobody hears it" situation).
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `HttpResponse` instead.
*/
abstract class Range extends HTTP::Server::HttpResponse::Range {
/** Gets the data-flow node that specifies the location of this HTTP redirect response. */
abstract DataFlow::Node getRedirectLocation();
}
}
}
}

View File

@@ -35,7 +35,7 @@ private module Django {
* WARNING: Only holds for a few predefined attributes.
*/
private DataFlow::Node django_attr(DataFlow::TypeTracker t, string attr_name) {
attr_name in ["db", "urls", "http", "conf", "views"] and
attr_name in ["db", "urls", "http", "conf", "views", "shortcuts"] and
(
t.start() and
result = DataFlow::importNode("django" + "." + attr_name)
@@ -724,7 +724,8 @@ private module Django {
*
* Use the predicate `HttpResponseRedirect::instance()` to get references to instances of `django.http.response.HttpResponseRedirect`.
*/
abstract class InstanceSource extends HttpResponse::InstanceSource, DataFlow::Node { }
abstract class InstanceSource extends HttpResponse::InstanceSource,
HTTP::Server::HttpRedirectResponse::Range, DataFlow::Node { }
/** A direct instantiation of `django.http.response.HttpResponseRedirect`. */
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
@@ -739,6 +740,10 @@ private module Django {
result.asCfgNode() in [node.getArg(1), node.getArgByName("content")]
}
override DataFlow::Node getRedirectLocation() {
result.asCfgNode() in [node.getArg(0), node.getArgByName("redirect_to")]
}
// How to support the `headers` argument here?
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
@@ -790,7 +795,8 @@ private module Django {
*
* Use the predicate `HttpResponsePermanentRedirect::instance()` to get references to instances of `django.http.response.HttpResponsePermanentRedirect`.
*/
abstract class InstanceSource extends HttpResponse::InstanceSource, DataFlow::Node { }
abstract class InstanceSource extends HttpResponse::InstanceSource,
HTTP::Server::HttpRedirectResponse::Range, DataFlow::Node { }
/** A direct instantiation of `django.http.response.HttpResponsePermanentRedirect`. */
private class ClassInstantiation extends InstanceSource, DataFlow::CfgNode {
@@ -805,6 +811,10 @@ private module Django {
result.asCfgNode() in [node.getArg(1), node.getArgByName("content")]
}
override DataFlow::Node getRedirectLocation() {
result.asCfgNode() in [node.getArg(0), node.getArgByName("redirect_to")]
}
// How to support the `headers` argument here?
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
@@ -1907,6 +1917,62 @@ private module Django {
}
}
}
// -------------------------------------------------------------------------
// django.shortcuts
// -------------------------------------------------------------------------
/** Gets a reference to the `django.shortcuts` module. */
DataFlow::Node shortcuts() { result = django_attr("shortcuts") }
/** Provides models for the `django.shortcuts` module */
module shortcuts {
/**
* Gets a reference to the attribute `attr_name` of the `django.shortcuts` module.
* WARNING: Only holds for a few predefined attributes.
*/
private DataFlow::Node shortcuts_attr(DataFlow::TypeTracker t, string attr_name) {
attr_name in ["redirect"] and
(
t.start() and
result = DataFlow::importNode("django.shortcuts" + "." + attr_name)
or
t.startInAttr(attr_name) and
result = shortcuts()
)
or
// Due to bad performance when using normal setup with `shortcuts_attr(t2, attr_name).track(t2, t)`
// we have inlined that code and forced a join
exists(DataFlow::TypeTracker t2 |
exists(DataFlow::StepSummary summary |
shortcuts_attr_first_join(t2, attr_name, result, summary) and
t = t2.append(summary)
)
)
}
pragma[nomagic]
private predicate shortcuts_attr_first_join(
DataFlow::TypeTracker t2, string attr_name, DataFlow::Node res,
DataFlow::StepSummary summary
) {
DataFlow::StepSummary::step(shortcuts_attr(t2, attr_name), res, summary)
}
/**
* Gets a reference to the attribute `attr_name` of the `django.shortcuts` module.
* WARNING: Only holds for a few predefined attributes.
*/
private DataFlow::Node shortcuts_attr(string attr_name) {
result = shortcuts_attr(DataFlow::TypeTracker::end(), attr_name)
}
/**
* Gets a reference to the `django.shortcuts.redirect` function
*
* See https://docs.djangoproject.com/en/3.1/topics/http/shortcuts/#redirect
*/
DataFlow::Node redirect() { result = shortcuts_attr("redirect") }
}
}
// ---------------------------------------------------------------------------
@@ -2230,4 +2296,39 @@ private module Django {
)
}
}
// ---------------------------------------------------------------------------
// django.shortcuts.redirect
// ---------------------------------------------------------------------------
/**
* A call to `django.shortcuts.redirect`.
*
* Note: This works differently depending on what argument is used.
* _One_ option is to redirect to a full URL.
*
* See https://docs.djangoproject.com/en/3.1/topics/http/shortcuts/#redirect
*/
private class DjangoShortcutsRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
DataFlow::CfgNode {
override CallNode node;
DjangoShortcutsRedirectCall() { node.getFunction() = django::shortcuts::redirect().asCfgNode() }
/**
* Gets the data-flow node that specifies the location of this HTTP redirect response.
*
* Note: For `django.shortcuts.redirect`, the result might not be a full URL
* (as usually expected by this method), but could be a relative URL,
* a string identifying a view, or a Django model.
*/
override DataFlow::Node getRedirectLocation() {
result.asCfgNode() in [node.getArg(0), node.getArgByName("to")]
}
override DataFlow::Node getBody() { none() }
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
override string getMimetypeDefault() { none() }
}
}

View File

@@ -34,7 +34,7 @@ private module FlaskModel {
* WARNING: Only holds for a few predefined attributes.
*/
private DataFlow::Node flask_attr(DataFlow::TypeTracker t, string attr_name) {
attr_name in ["request", "make_response", "Response", "views"] and
attr_name in ["request", "make_response", "Response", "views", "redirect"] and
(
t.start() and
result = DataFlow::importNode("flask" + "." + attr_name)
@@ -669,4 +669,31 @@ private module FlaskModel {
override string getMimetypeDefault() { result = "text/html" }
}
/**
* A call to the `flask.redirect` function.
*
* See https://flask.palletsprojects.com/en/1.1.x/api/#flask.redirect
*/
private class FlaskRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
DataFlow::CfgNode {
override CallNode node;
FlaskRedirectCall() { node.getFunction() = flask_attr("redirect").asCfgNode() }
override DataFlow::Node getRedirectLocation() {
result.asCfgNode() in [node.getArg(0), node.getArgByName("location")]
}
override DataFlow::Node getBody() { none() }
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
override string getMimetypeDefault() {
// note that while you're not able to set content yourself, the function will
// actually fill out some default content, that is served with mimetype
// `text/html`.
result = "text/html"
}
}
}

View File

@@ -216,6 +216,17 @@ private module Tornado {
/** Gets a reference to one of the methods `get_arguments`, `get_body_arguments`, `get_query_arguments`. */
DataFlow::Node argumentsMethod() { result = argumentsMethod(DataFlow::TypeTracker::end()) }
/** Gets a reference the `redirect` method. */
private DataFlow::Node redirectMethod(DataFlow::TypeTracker t) {
t.startInAttr("redirect") and
result = instance()
or
exists(DataFlow::TypeTracker t2 | result = redirectMethod(t2).track(t2, t))
}
/** Gets a reference the `redirect` method. */
DataFlow::Node redirectMethod() { result = redirectMethod(DataFlow::TypeTracker::end()) }
/** Gets a reference to the `write` method. */
private DataFlow::Node writeMethod(DataFlow::TypeTracker t) {
t.startInAttr("write") and
@@ -556,7 +567,31 @@ private module Tornado {
// Response modeling
// ---------------------------------------------------------------------------
/**
* A call to `tornado.web.RequestHandler.write` method.
* A call to the `tornado.web.RequestHandler.redirect` method.
*
* See https://www.tornadoweb.org/en/stable/web.html?highlight=write#tornado.web.RequestHandler.redirect
*/
private class TornadoRequestHandlerRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
DataFlow::CfgNode {
override CallNode node;
TornadoRequestHandlerRedirectCall() {
node.getFunction() = tornado::web::RequestHandler::redirectMethod().asCfgNode()
}
override DataFlow::Node getRedirectLocation() {
result.asCfgNode() in [node.getArg(0), node.getArgByName("url")]
}
override DataFlow::Node getBody() { none() }
override string getMimetypeDefault() { none() }
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
}
/**
* A call to the `tornado.web.RequestHandler.write` method.
*
* See https://www.tornadoweb.org/en/stable/web.html?highlight=write#tornado.web.RequestHandler.write
*/

View File

@@ -0,0 +1,37 @@
/**
* Provides a taint-tracking configuration for detecting URL redirection
* vulnerabilities.
*/
import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import semmle.python.Concepts
import semmle.python.dataflow.new.RemoteFlowSources
import semmle.python.dataflow.new.BarrierGuards
/**
* A taint-tracking configuration for detecting URL redirection vulnerabilities.
*/
class UrlRedirectConfiguration extends TaintTracking::Configuration {
UrlRedirectConfiguration() { this = "UrlRedirectConfiguration" }
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
override predicate isSink(DataFlow::Node sink) {
sink = any(HTTP::Server::HttpRedirectResponse e).getRedirectLocation()
}
override predicate isSanitizer(DataFlow::Node node) {
// Url redirection is a problem only if the user controls the prefix of the URL.
// TODO: This is a copy of the taint-sanitizer from the old points-to query, which doesn't
// cover formatting.
exists(BinaryExprNode string_concat | string_concat.getOp() instanceof Add |
string_concat.getRight() = node.asCfgNode()
)
}
override predicate isSanitizerGuard(DataFlow::BarrierGuard guard) {
guard instanceof StringConstCompare
}
}

View File

@@ -1,4 +1,5 @@
from django.http.response import HttpResponse, HttpResponseRedirect, JsonResponse, HttpResponseNotFound
from django.http.response import HttpResponse, HttpResponseRedirect, HttpResponsePermanentRedirect, JsonResponse, HttpResponseNotFound
import django.shortcuts
# Not an XSS sink, since the Content-Type is not "text/html"
# FP reported in https://github.com/github/codeql-python-team/issues/38
@@ -18,15 +19,40 @@ def safe__manual_content_type(request):
# XSS FP reported in https://github.com/github/codeql/issues/3466
# Note: This should be an open-redirect sink, but not an XSS sink.
def or__redirect(request):
return HttpResponseRedirect(request.GET.get("next")) # $HttpResponse mimetype=text/html
next = request.GET.get("next")
return HttpResponseRedirect(next) # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation=next
def information_exposure_through_redirect(request, as_kw=False):
# This is a contrived example, but possible
private = "private"
next = request.GET.get("next")
if as_kw:
return HttpResponseRedirect(request.GET.get("next"), content=private) # $HttpResponse mimetype=text/html responseBody=private
return HttpResponseRedirect(next, content=private) # $HttpResponse mimetype=text/html responseBody=private HttpRedirectResponse redirectLocation=next
else:
return HttpResponseRedirect(request.GET.get("next"), private) # $HttpResponse mimetype=text/html responseBody=private
return HttpResponseRedirect(next, private) # $ HttpResponse mimetype=text/html responseBody=private HttpRedirectResponse redirectLocation=next
def perm_redirect(request):
private = "private"
next = request.GET.get("next")
return HttpResponsePermanentRedirect(next, private) # $ HttpResponse mimetype=text/html responseBody=private HttpRedirectResponse redirectLocation=next
def redirect_through_normal_response(request):
private = "private"
next = request.GET.get("next")
resp = HttpResponse() # $ HttpResponse mimetype=text/html
resp.status_code = 302
resp['Location'] = next # $ MISSING: redirectLocation=next
resp.content = private # $ MISSING: responseBody=private
return resp
def redirect_shortcut(request):
next = request.GET.get("next")
return django.shortcuts.redirect(next) # $ HttpResponse HttpRedirectResponse redirectLocation=next
# Ensure that simple subclasses are still vuln to XSS
def xss__not_found(request):

View File

@@ -1,6 +1,6 @@
import json
from flask import Flask, make_response, jsonify, Response, request
from flask import Flask, make_response, jsonify, Response, request, redirect
app = Flask(__name__)
@@ -172,6 +172,18 @@ def app_response_class(): # $requestHandler
# TODO: add tests for setting status code
# TODO: add test that manually creates a redirect by setting status code and suitable header.
################################################################################
# Redirect
################################################################################
@app.route("/redirect-simple") # $routeSetup="/redirect-simple"
def redirect_simple(): # $requestHandler
next = request.args['next']
resp = redirect(next) # $ HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation=next
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
################################################################################

View File

@@ -25,7 +25,7 @@ class RedirectHandler(tornado.web.RequestHandler):
req = self.request
h = req.headers
url = h["url"]
self.redirect(url)
self.redirect(url) # $ HttpRedirectResponse HttpResponse redirectLocation=url
class BaseReverseInheritance(tornado.web.RequestHandler):

View File

@@ -33,7 +33,8 @@ class ExplicitContentType(tornado.web.RequestHandler):
class ExampleRedirect(tornado.web.RequestHandler):
def get(self): # $ requestHandler
self.redirect("http://example.com") # TODO: Model redirect
url = "http://example.com"
self.redirect(url) # $ HttpRedirectResponse HttpResponse redirectLocation=url
class ExampleConnectionWrite(tornado.web.RequestHandler):

View File

@@ -239,6 +239,31 @@ class HttpServerHttpResponseTest extends InlineExpectationsTest {
}
}
class HttpServerHttpRedirectResponseTest extends InlineExpectationsTest {
HttpServerHttpRedirectResponseTest() { this = "HttpServerHttpRedirectResponseTest" }
override string getARelevantTag() { result in ["HttpRedirectResponse", "redirectLocation"] }
override predicate hasActualResult(Location location, string element, string tag, string value) {
exists(location.getFile().getRelativePath()) and
(
exists(HTTP::Server::HttpRedirectResponse redirect |
location = redirect.getLocation() and
element = redirect.toString() and
value = "" and
tag = "HttpRedirectResponse"
)
or
exists(HTTP::Server::HttpRedirectResponse redirect |
location = redirect.getLocation() and
element = redirect.toString() and
value = value_from_expr(redirect.getRedirectLocation().asExpr()) and
tag = "redirectLocation"
)
)
}
}
class FileSystemAccessTest extends InlineExpectationsTest {
FileSystemAccessTest() { this = "FileSystemAccessTest" }

View File

@@ -1,7 +1,35 @@
edges
| test.py:7:14:7:25 | dict of externally controlled string | test.py:7:14:7:43 | externally controlled string |
| test.py:7:14:7:25 | dict of externally controlled string | test.py:7:14:7:43 | externally controlled string |
| test.py:7:14:7:43 | externally controlled string | test.py:8:21:8:26 | externally controlled string |
| test.py:7:14:7:43 | externally controlled string | test.py:8:21:8:26 | externally controlled string |
| test.py:7:14:7:25 | ControlFlowNode for Attribute | test.py:8:21:8:26 | ControlFlowNode for target |
| test.py:30:17:30:28 | ControlFlowNode for Attribute | test.py:32:21:32:24 | ControlFlowNode for safe |
| test.py:37:17:37:28 | ControlFlowNode for Attribute | test.py:39:21:39:24 | ControlFlowNode for safe |
| test.py:44:17:44:28 | ControlFlowNode for Attribute | test.py:46:21:46:24 | ControlFlowNode for safe |
| test.py:60:17:60:28 | ControlFlowNode for Attribute | test.py:62:21:62:26 | ControlFlowNode for unsafe |
| test.py:67:17:67:28 | ControlFlowNode for Attribute | test.py:69:21:69:26 | ControlFlowNode for unsafe |
| test.py:74:17:74:28 | ControlFlowNode for Attribute | test.py:76:21:76:26 | ControlFlowNode for unsafe |
| test.py:81:17:81:28 | ControlFlowNode for Attribute | test.py:83:21:83:26 | ControlFlowNode for unsafe |
nodes
| test.py:7:14:7:25 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:8:21:8:26 | ControlFlowNode for target | semmle.label | ControlFlowNode for target |
| test.py:30:17:30:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:32:21:32:24 | ControlFlowNode for safe | semmle.label | ControlFlowNode for safe |
| test.py:37:17:37:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:39:21:39:24 | ControlFlowNode for safe | semmle.label | ControlFlowNode for safe |
| test.py:44:17:44:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:46:21:46:24 | ControlFlowNode for safe | semmle.label | ControlFlowNode for safe |
| test.py:60:17:60:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:62:21:62:26 | ControlFlowNode for unsafe | semmle.label | ControlFlowNode for unsafe |
| test.py:67:17:67:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:69:21:69:26 | ControlFlowNode for unsafe | semmle.label | ControlFlowNode for unsafe |
| test.py:74:17:74:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:76:21:76:26 | ControlFlowNode for unsafe | semmle.label | ControlFlowNode for unsafe |
| test.py:81:17:81:28 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
| test.py:83:21:83:26 | ControlFlowNode for unsafe | semmle.label | ControlFlowNode for unsafe |
#select
| test.py:8:21:8:26 | target | test.py:7:14:7:25 | dict of externally controlled string | test.py:8:21:8:26 | externally controlled string | Untrusted URL redirection due to $@. | test.py:7:14:7:25 | Attribute | a user-provided value |
| test.py:8:21:8:26 | ControlFlowNode for target | test.py:7:14:7:25 | ControlFlowNode for Attribute | test.py:8:21:8:26 | ControlFlowNode for target | Untrusted URL redirection due to $@. | test.py:7:14:7:25 | ControlFlowNode for Attribute | A user-provided value |
| test.py:32:21:32:24 | ControlFlowNode for safe | test.py:30:17:30:28 | ControlFlowNode for Attribute | test.py:32:21:32:24 | ControlFlowNode for safe | Untrusted URL redirection due to $@. | test.py:30:17:30:28 | ControlFlowNode for Attribute | A user-provided value |
| test.py:39:21:39:24 | ControlFlowNode for safe | test.py:37:17:37:28 | ControlFlowNode for Attribute | test.py:39:21:39:24 | ControlFlowNode for safe | Untrusted URL redirection due to $@. | test.py:37:17:37:28 | ControlFlowNode for Attribute | A user-provided value |
| test.py:46:21:46:24 | ControlFlowNode for safe | test.py:44:17:44:28 | ControlFlowNode for Attribute | test.py:46:21:46:24 | ControlFlowNode for safe | Untrusted URL redirection due to $@. | test.py:44:17:44:28 | ControlFlowNode for Attribute | A user-provided value |
| test.py:62:21:62:26 | ControlFlowNode for unsafe | test.py:60:17:60:28 | ControlFlowNode for Attribute | test.py:62:21:62:26 | ControlFlowNode for unsafe | Untrusted URL redirection due to $@. | test.py:60:17:60:28 | ControlFlowNode for Attribute | A user-provided value |
| test.py:69:21:69:26 | ControlFlowNode for unsafe | test.py:67:17:67:28 | ControlFlowNode for Attribute | test.py:69:21:69:26 | ControlFlowNode for unsafe | Untrusted URL redirection due to $@. | test.py:67:17:67:28 | ControlFlowNode for Attribute | A user-provided value |
| test.py:76:21:76:26 | ControlFlowNode for unsafe | test.py:74:17:74:28 | ControlFlowNode for Attribute | test.py:76:21:76:26 | ControlFlowNode for unsafe | Untrusted URL redirection due to $@. | test.py:74:17:74:28 | ControlFlowNode for Attribute | A user-provided value |
| test.py:83:21:83:26 | ControlFlowNode for unsafe | test.py:81:17:81:28 | ControlFlowNode for Attribute | test.py:83:21:83:26 | ControlFlowNode for unsafe | Untrusted URL redirection due to $@. | test.py:81:17:81:28 | ControlFlowNode for Attribute | A user-provided value |

View File

@@ -1 +1 @@
semmle-extractor-options: --max-import-depth=2 -p ../lib
semmle-extractor-options: --lang=3 --max-import-depth=2 -p ../lib

View File

@@ -8,11 +8,76 @@ def hello():
return redirect(target, code=302)
#Check for safe prefixes
# Check for safe prefixes
@app.route('/ok')
def ok():
untrusted = request.args.get('ok', '')
safe = "safe"
untrusted = request.args.get('target', '')
safe = "https://safe.com/"
safe += untrusted
return redirect(safe, code=302)
@app.route('/ok2')
def ok2():
untrusted = request.args.get('target', '')
safe = "https://safe.com/" + untrusted
return redirect(safe, code=302)
@app.route('/ok3')
def ok3():
untrusted = request.args.get('target', '')
safe = "https://safe.com/{}".format(untrusted)
return redirect(safe, code=302) # FP
@app.route('/ok4')
def ok4():
untrusted = request.args.get('target', '')
safe = f"https://safe.com/{untrusted}"
return redirect(safe, code=302) # FP
@app.route('/ok5')
def ok5():
untrusted = request.args.get('target', '')
safe = "https://safe.com/%s" % untrusted
return redirect(safe, code=302) # FP
@app.route('/const-str-compare')
def const_str_compare():
target = request.args.get('target', '')
if target == "example.com/":
return redirect(target, code=302)
# Check that our sanitizer is not too broad
@app.route('/not_ok1')
def not_ok1():
untrusted = request.args.get('target', '')
unsafe = untrusted + "?login=success"
return redirect(unsafe, code=302)
@app.route('/not_ok2')
def not_ok2():
untrusted = request.args.get('target', '')
unsafe = "{}?login=success".format(untrusted)
return redirect(unsafe, code=302)
@app.route('/not_ok3')
def not_ok3():
untrusted = request.args.get('target', '')
unsafe = f"{untrusted}?login=success"
return redirect(unsafe, code=302)
@app.route('/not_ok4')
def not_ok4():
untrusted = request.args.get('target', '')
unsafe = "%s?login=success" % untrusted
return redirect(unsafe, code=302)