mirror of
https://github.com/github/codeql.git
synced 2025-12-17 01:03:14 +01:00
Merge pull request #4981 from RasmusWL/port-url-redirect-query
Python: Port url redirect query
This commit is contained in:
@@ -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.
|
||||
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
################################################################################
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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" }
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -1 +1 @@
|
||||
semmle-extractor-options: --max-import-depth=2 -p ../lib
|
||||
semmle-extractor-options: --lang=3 --max-import-depth=2 -p ../lib
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user