mirror of
https://github.com/github/codeql.git
synced 2026-04-25 08:45:14 +02:00
Merge branch 'main' of https://github.com/github/codeql into python/test-constructor-MaD-path
This commit is contained in:
@@ -1,3 +1,14 @@
|
||||
## 0.12.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Deleted the deprecated `RegExpPatterns` module from `Regexp.qll`.
|
||||
* Deleted the deprecated `Security/CWE-020/HostnameRegexpShared.qll` file.
|
||||
|
||||
### Deprecated APIs
|
||||
|
||||
- Renamed the `StrConst` class to `StringLiteral`, for greater consistency with other languages. The `StrConst` and `Str` classes are now deprecated and will be removed in a future release.
|
||||
|
||||
## 0.11.14
|
||||
|
||||
### Minor Analysis Improvements
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
category: deprecated
|
||||
---
|
||||
|
||||
- Renamed the `StrConst` class to `StringLiteral`, for greater consistency with other languages. The `StrConst` and `Str` classes are now deprecated and will be removed in a future release.
|
||||
4
python/ql/lib/change-notes/2024-04-30-pyramid.md
Normal file
4
python/ql/lib/change-notes/2024-04-30-pyramid.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: majorAnalysis
|
||||
---
|
||||
* Added modeling of the `pyramid` framework, leading to new remote flow sources and sinks.
|
||||
10
python/ql/lib/change-notes/released/0.12.0.md
Normal file
10
python/ql/lib/change-notes/released/0.12.0.md
Normal file
@@ -0,0 +1,10 @@
|
||||
## 0.12.0
|
||||
|
||||
### Breaking Changes
|
||||
|
||||
* Deleted the deprecated `RegExpPatterns` module from `Regexp.qll`.
|
||||
* Deleted the deprecated `Security/CWE-020/HostnameRegexpShared.qll` file.
|
||||
|
||||
### Deprecated APIs
|
||||
|
||||
- Renamed the `StrConst` class to `StringLiteral`, for greater consistency with other languages. The `StrConst` and `Str` classes are now deprecated and will be removed in a future release.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.11.14
|
||||
lastReleaseVersion: 0.12.0
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-all
|
||||
version: 0.11.15-dev
|
||||
version: 0.12.1-dev
|
||||
groups: python
|
||||
dbscheme: semmlecode.python.dbscheme
|
||||
extractor: python
|
||||
|
||||
@@ -1025,6 +1025,114 @@ module Http {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sets a header in an HTTP response.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `ResponseHeaderWrite::Range` instead.
|
||||
*/
|
||||
class ResponseHeaderWrite extends DataFlow::Node instanceof ResponseHeaderWrite::Range {
|
||||
/**
|
||||
* Gets the argument containing the header name.
|
||||
*/
|
||||
DataFlow::Node getNameArg() { result = super.getNameArg() }
|
||||
|
||||
/**
|
||||
* Gets the argument containing the header value.
|
||||
*/
|
||||
DataFlow::Node getValueArg() { result = super.getValueArg() }
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header name argument.
|
||||
*/
|
||||
predicate nameAllowsNewline() { super.nameAllowsNewline() }
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header value argument.
|
||||
*/
|
||||
predicate valueAllowsNewline() { super.valueAllowsNewline() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling header writes on HTTP responses. */
|
||||
module ResponseHeaderWrite {
|
||||
/**
|
||||
*A data-flow node that sets a header in an HTTP response.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `ResponseHeaderWrite` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets the argument containing the header name.
|
||||
*/
|
||||
abstract DataFlow::Node getNameArg();
|
||||
|
||||
/**
|
||||
* Gets the argument containing the header value.
|
||||
*/
|
||||
abstract DataFlow::Node getValueArg();
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header name argument.
|
||||
*/
|
||||
abstract predicate nameAllowsNewline();
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header value argument.
|
||||
*/
|
||||
abstract predicate valueAllowsNewline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sets multiple headers in an HTTP response using a dict or a list of tuples.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `ResponseHeaderBulkWrite::Range` instead.
|
||||
*/
|
||||
class ResponseHeaderBulkWrite extends DataFlow::Node instanceof ResponseHeaderBulkWrite::Range {
|
||||
/**
|
||||
* Gets the argument containing the headers dictionary.
|
||||
*/
|
||||
DataFlow::Node getBulkArg() { result = super.getBulkArg() }
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header name argument.
|
||||
*/
|
||||
predicate nameAllowsNewline() { super.nameAllowsNewline() }
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header value argument.
|
||||
*/
|
||||
predicate valueAllowsNewline() { super.valueAllowsNewline() }
|
||||
}
|
||||
|
||||
/** Provides a class for modeling bulk header writes on HTTP responses. */
|
||||
module ResponseHeaderBulkWrite {
|
||||
/**
|
||||
* A data-flow node that sets multiple headers in an HTTP response using a dict.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `ResponseHeaderBulkWrite` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets the argument containing the headers dictionary.
|
||||
*/
|
||||
abstract DataFlow::Node getBulkArg();
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header name argument.
|
||||
*/
|
||||
abstract predicate nameAllowsNewline();
|
||||
|
||||
/**
|
||||
* Holds if newlines are accepted in the header value argument.
|
||||
*/
|
||||
abstract predicate valueAllowsNewline();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that sets a cookie in an HTTP response.
|
||||
*
|
||||
|
||||
@@ -56,6 +56,7 @@ private import semmle.python.frameworks.PyMongo
|
||||
private import semmle.python.frameworks.Pymssql
|
||||
private import semmle.python.frameworks.PyMySQL
|
||||
private import semmle.python.frameworks.Pyodbc
|
||||
private import semmle.python.frameworks.Pyramid
|
||||
private import semmle.python.frameworks.Requests
|
||||
private import semmle.python.frameworks.RestFramework
|
||||
private import semmle.python.frameworks.Rsa
|
||||
|
||||
@@ -7,20 +7,6 @@ private import semmle.python.regex
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.regexp.internal.RegExpTracking
|
||||
|
||||
/**
|
||||
* Provides utility predicates related to regular expressions.
|
||||
*/
|
||||
deprecated module RegExpPatterns {
|
||||
/**
|
||||
* Gets a pattern that matches common top-level domain names in lower case.
|
||||
* DEPRECATED: use the similarly named predicate from `HostnameRegex` from the `regex` pack instead.
|
||||
*/
|
||||
deprecated string getACommonTld() {
|
||||
// according to ranking by http://google.com/search?q=site:.<<TLD>>
|
||||
result = "(?:com|org|edu|gov|uk|net|io)(?![a-z0-9])"
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A node whose value may flow to a position where it is interpreted
|
||||
* as a part of a regular expression.
|
||||
|
||||
@@ -220,6 +220,43 @@ module Flask {
|
||||
|
||||
/** Gets a reference to an instance of `flask.Response`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** An `Headers` instance that is part of a Flask response. */
|
||||
private class FlaskResponseHeadersInstances extends Werkzeug::Headers::InstanceSource {
|
||||
FlaskResponseHeadersInstances() {
|
||||
this.(DataFlow::AttrRead).getObject() = instance() and
|
||||
this.(DataFlow::AttrRead).getAttributeName() = "headers"
|
||||
}
|
||||
}
|
||||
|
||||
/** A class instantiation of `Response` that sets response headers. */
|
||||
private class ResponseClassHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range,
|
||||
ClassInstantiation
|
||||
{
|
||||
override DataFlow::Node getBulkArg() {
|
||||
result = [this.getArg(2), this.getArgByName("headers")]
|
||||
}
|
||||
|
||||
override predicate nameAllowsNewline() { any() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
|
||||
/** A call to `make_response that sets response headers. */
|
||||
private class MakeResponseHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range,
|
||||
FlaskMakeResponseCall
|
||||
{
|
||||
override DataFlow::Node getBulkArg() {
|
||||
result = this.getArg(2)
|
||||
or
|
||||
strictcount(this.getArg(_)) = 2 and
|
||||
result = this.getArg(1)
|
||||
}
|
||||
|
||||
override predicate nameAllowsNewline() { any() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
299
python/ql/lib/semmle/python/frameworks/Pyramid.qll
Normal file
299
python/ql/lib/semmle/python/frameworks/Pyramid.qll
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `pyramid` PyPI package.
|
||||
* See https://docs.pylonsproject.org/projects/pyramid/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.dataflow.new.FlowSummary
|
||||
private import semmle.python.frameworks.internal.PoorMansFunctionResolution
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
|
||||
/**
|
||||
* Provides models for the `pyramid` PyPI package.
|
||||
* See https://docs.pylonsproject.org/projects/pyramid/.
|
||||
*/
|
||||
module Pyramid {
|
||||
/** Provides models for pyramid View callables. */
|
||||
module View {
|
||||
/** A dataflow node that sets up a route on a server using the Pyramid framework. */
|
||||
abstract private class PyramidRouteSetup extends Http::Server::RouteSetup::Range {
|
||||
override string getFramework() { result = "Pyramid" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Pyramid view callable, that handles incoming requests.
|
||||
*/
|
||||
class ViewCallable extends Function {
|
||||
ViewCallable() { this = any(PyramidRouteSetup rs).getARequestHandler() }
|
||||
|
||||
/** Gets the `request` parameter of this callable. */
|
||||
Parameter getRequestParameter() {
|
||||
this.getPositionalParameterCount() = 1 and
|
||||
result = this.getArg(0)
|
||||
or
|
||||
this.getPositionalParameterCount() = 2 and
|
||||
result = this.getArg(1)
|
||||
}
|
||||
}
|
||||
|
||||
/** A pyramid route setup using the `pyramid.view.view_config` decorator. */
|
||||
private class DecoratorSetup extends PyramidRouteSetup {
|
||||
DecoratorSetup() {
|
||||
this = API::moduleImport("pyramid").getMember("view").getMember("view_config").getACall()
|
||||
}
|
||||
|
||||
override Function getARequestHandler() { result.getADecorator() = this.asExpr() }
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() { none() } // there is a `route_name` arg, but that does not contain the url pattern
|
||||
|
||||
override Parameter getARoutedParameter() { none() }
|
||||
}
|
||||
|
||||
/** A pyramid route setup using a call to `pyramid.config.Configurator.add_view`. */
|
||||
private class ConfiguratorSetup extends PyramidRouteSetup instanceof Configurator::AddViewCall {
|
||||
override Function getARequestHandler() {
|
||||
this.(Configurator::AddViewCall).getViewArg() = poorMansFunctionTracker(result)
|
||||
}
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() { none() } // there is a `route_name` arg, but that does not contain the url pattern
|
||||
|
||||
override Parameter getARoutedParameter() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides models for `pyramid.config.Configurator` */
|
||||
module Configurator {
|
||||
/** Gets a reference to the class `pyramid.config.Configurator`. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("pyramid").getMember("config").getMember("Configurator")
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.config.Configurator`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = classRef().getACall()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.config.Configurator`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** A call to the `add_view` method of an instance of `pyramid.config.Configurator`. */
|
||||
class AddViewCall extends DataFlow::MethodCallNode {
|
||||
AddViewCall() { this.calls(instance(), "add_view") }
|
||||
|
||||
/** Gets the `view` argument of this call. */
|
||||
DataFlow::Node getViewArg() { result = [this.getArg(0), this.getArgByName("view")] }
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides modeling for pyramid requests. */
|
||||
module Request {
|
||||
/**
|
||||
* A source of instances of `pyramid.request.Request`, extend this class to model new instances.
|
||||
*
|
||||
* Use the predicate `Request::instance()` to get references to instances of `pyramid.request.Request`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.request.Request`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.request.Request`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
private class RequestParameter extends InstanceSource, RemoteFlowSource::Range instanceof DataFlow::ParameterNode
|
||||
{
|
||||
RequestParameter() { this.getParameter() = any(View::ViewCallable vc).getRequestParameter() }
|
||||
|
||||
override string getSourceType() { result = "Pyramid request parameter" }
|
||||
}
|
||||
|
||||
/** Taint steps for request instances. */
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "pyramid.request.Request" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in [
|
||||
"accept", "accept_charset", "accept_encoding", "accept_language", "application_url",
|
||||
"as_bytes", "authorization", "body", "body_file", "body_file_raw", "body_file_seekable",
|
||||
"cache_control", "client_addr", "content_type", "cookies", "domain", "headers", "host",
|
||||
"host_port", "host_url", "GET", "if_match", "if_none_match", "if_range",
|
||||
"if_none_match", "json", "json_body", "matchdict", "params", "path", "path_info",
|
||||
"path_qs", "path_url", "POST", "pragma", "query_string", "range", "referer", "referrer",
|
||||
"text", "url", "urlargs", "urlvars", "user_agent"
|
||||
]
|
||||
}
|
||||
|
||||
override string getMethodName() {
|
||||
result in ["as_bytes", "copy", "copy_get", "path_info_peek", "path_info_pop"]
|
||||
}
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** A call to a method of a `request` that copies the request. */
|
||||
private class RequestCopyCall extends InstanceSource, DataFlow::MethodCallNode {
|
||||
RequestCopyCall() { this.calls(instance(), ["copy", "copy_get"]) }
|
||||
}
|
||||
|
||||
/** A member of a request that is a file-like object. */
|
||||
private class RequestBodyFileLike extends Stdlib::FileLikeObject::InstanceSource instanceof DataFlow::AttrRead
|
||||
{
|
||||
RequestBodyFileLike() {
|
||||
this.getObject() = instance() and
|
||||
this.getAttributeName() = ["body_file", "body_file_raw", "body_file_seekable"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides modeling for pyramid responses. */
|
||||
module Response {
|
||||
/** A response returned by a view callable. */
|
||||
private class PyramidReturnResponse extends Http::Server::HttpResponse::Range {
|
||||
PyramidReturnResponse() {
|
||||
this.asCfgNode() = any(View::ViewCallable vc).getAReturnValueFlowNode() and
|
||||
not this = instance()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { result = this }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** Gets a reference to the class `pyramid.response.Response`. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("pyramid").getMember("response").getMember("Response")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `pyramid.response.Response`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Response::instance()` to get references to instances of `pyramid.response.Response`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode,
|
||||
Http::Server::HttpResponse::Range
|
||||
{ }
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.response.Response`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.response.Response`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** An instantiation of the class `pyramid.response.Response` or a subclass. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getBody() { result = [this.getArg(0), this.getArgByName("body")] }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
result = [this.getArg(4), this.getArgByName("content_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** A write to a field that sets the body of a response. */
|
||||
private class ResponseBodySet extends Http::Server::HttpResponse::Range instanceof DataFlow::AttrWrite
|
||||
{
|
||||
string attrName;
|
||||
|
||||
ResponseBodySet() {
|
||||
this.getObject() = instance() and
|
||||
this.getAttributeName() = attrName and
|
||||
attrName in ["body", "body_file", "json", "json_body", "text", "ubody", "unicode_body"]
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { result = this.(DataFlow::AttrWrite).getValue() }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() {
|
||||
if attrName in ["json", "json_body"]
|
||||
then result = "application/json"
|
||||
else result = "text/html"
|
||||
}
|
||||
}
|
||||
|
||||
/** A use of the `response` attribute of a `Request`. */
|
||||
private class RequestResponseAttr extends InstanceSource instanceof DataFlow::AttrRead {
|
||||
RequestResponseAttr() {
|
||||
this.getObject() = Request::instance() and this.getAttributeName() = "response"
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** A call to `response.set_cookie`. */
|
||||
private class SetCookieCall extends Http::Server::CookieWrite::Range, DataFlow::MethodCallNode {
|
||||
SetCookieCall() { this.calls(instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
override DataFlow::Node getNameArg() { result = [this.getArg(0), this.getArgByName("name")] }
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result = [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides models for pyramid http redirects. */
|
||||
module Redirect {
|
||||
/** Gets a reference to a class that represents an HTTP redirect response.. */
|
||||
API::Node classRef() {
|
||||
result =
|
||||
API::moduleImport("pyramid")
|
||||
.getMember("httpexceptions")
|
||||
.getMember([
|
||||
"HTTPMultipleChoices", "HTTPMovedPermanently", "HTTPFound", "HTTPSeeOther",
|
||||
"HTTPUseProxy", "HTTPTemporaryRedirect", "HTTPPermanentRedirect"
|
||||
])
|
||||
}
|
||||
|
||||
/** A call to a pyramid HTTP exception class that represents an HTTP redirect response. */
|
||||
class PyramidRedirect extends Http::Server::HttpRedirectResponse::Range, DataFlow::CallCfgNode {
|
||||
PyramidRedirect() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getRedirectLocation() {
|
||||
result = [this.getArg(0), this.getArgByName("location")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2183,17 +2183,35 @@ module StdlibPrivate {
|
||||
* for how a request is processed and given to an application.
|
||||
*/
|
||||
class WsgirefSimpleServerApplication extends Http::Server::RequestHandler::Range {
|
||||
boolean validator;
|
||||
|
||||
WsgirefSimpleServerApplication() {
|
||||
exists(DataFlow::Node appArg, DataFlow::CallCfgNode setAppCall |
|
||||
(
|
||||
setAppCall =
|
||||
WsgirefSimpleServer::subclassRef().getReturn().getMember("set_app").getACall()
|
||||
WsgirefSimpleServer::subclassRef().getReturn().getMember("set_app").getACall() and
|
||||
validator = false
|
||||
or
|
||||
setAppCall
|
||||
.(DataFlow::MethodCallNode)
|
||||
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app")
|
||||
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app") and
|
||||
validator = false
|
||||
or
|
||||
// assume an application that is passed to `wsgiref.validate.validator` is eventually passed to `set_app`
|
||||
setAppCall =
|
||||
API::moduleImport("wsgiref").getMember("validate").getMember("validator").getACall() and
|
||||
validator = true
|
||||
) and
|
||||
appArg in [setAppCall.getArg(0), setAppCall.getArgByName("application")]
|
||||
or
|
||||
// `make_server` calls `set_app`
|
||||
setAppCall =
|
||||
API::moduleImport("wsgiref")
|
||||
.getMember("simple_server")
|
||||
.getMember("make_server")
|
||||
.getACall() and
|
||||
appArg in [setAppCall.getArg(2), setAppCall.getArgByName("app")] and
|
||||
validator = false
|
||||
|
|
||||
appArg = poorMansFunctionTracker(this)
|
||||
)
|
||||
@@ -2202,6 +2220,9 @@ module StdlibPrivate {
|
||||
override Parameter getARoutedParameter() { none() }
|
||||
|
||||
override string getFramework() { result = "Stdlib: wsgiref.simple_server application" }
|
||||
|
||||
/** Holds if this simple server application was passed to `wsgiref.validate.validator`. */
|
||||
predicate isValidated() { validator = true }
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -2305,6 +2326,114 @@ module StdlibPrivate {
|
||||
|
||||
override string getMimetypeDefault() { none() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Provides models for the `wsgiref.headers.Headers` class
|
||||
*
|
||||
* See https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers.
|
||||
*/
|
||||
module Headers {
|
||||
/** Gets a reference to the `wsgiref.headers.Headers` class. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("wsgiref").getMember("headers").getMember("Headers")
|
||||
or
|
||||
result = ModelOutput::getATypeNode("wsgiref.headers.Headers~Subclass").getASubclass*()
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = classRef().getACall()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** Holds if there exists an application that is validated by `wsgiref.validate.validator`. */
|
||||
private predicate existsValidatedApplication() {
|
||||
exists(WsgirefSimpleServerApplication app | app.isValidated())
|
||||
}
|
||||
|
||||
/** A class instantiation of `wsgiref.headers.Headers`, conidered as a write to a response header. */
|
||||
private class WsgirefHeadersInstantiation extends Http::Server::ResponseHeaderBulkWrite::Range,
|
||||
DataFlow::CallCfgNode
|
||||
{
|
||||
WsgirefHeadersInstantiation() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getBulkArg() {
|
||||
result = [this.getArg(0), this.getArgByName("headers")]
|
||||
}
|
||||
|
||||
// TODO: These checks perhaps could be made more precise.
|
||||
override predicate nameAllowsNewline() { not existsValidatedApplication() }
|
||||
|
||||
override predicate valueAllowsNewline() { not existsValidatedApplication() }
|
||||
}
|
||||
|
||||
/** A call to a method that writes to a response header. */
|
||||
private class HeaderWriteCall extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
HeaderWriteCall() {
|
||||
this.calls(instance(), ["add_header", "set", "setdefault", "__setitem__"])
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = this.getArg(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.getArg(1) }
|
||||
|
||||
// TODO: These checks perhaps could be made more precise.
|
||||
override predicate nameAllowsNewline() { not existsValidatedApplication() }
|
||||
|
||||
override predicate valueAllowsNewline() { not existsValidatedApplication() }
|
||||
}
|
||||
|
||||
/** A dict-like write to a response header. */
|
||||
private class HeaderWriteSubscript extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::Node
|
||||
{
|
||||
DataFlow::Node name;
|
||||
DataFlow::Node value;
|
||||
|
||||
HeaderWriteSubscript() {
|
||||
exists(SubscriptNode subscript |
|
||||
this.asCfgNode() = subscript and
|
||||
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
|
||||
name.asCfgNode() = subscript.getIndex() and
|
||||
subscript.getObject() = instance().asCfgNode()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = name }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
// TODO: These checks perhaps could be made more precise.
|
||||
override predicate nameAllowsNewline() { not existsValidatedApplication() }
|
||||
|
||||
override predicate valueAllowsNewline() { not existsValidatedApplication() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to a `start_response` function that sets the response headers.
|
||||
*/
|
||||
private class WsgirefSimpleServerSetHeaders extends Http::Server::ResponseHeaderBulkWrite::Range,
|
||||
DataFlow::CallCfgNode
|
||||
{
|
||||
WsgirefSimpleServerSetHeaders() { this.getFunction() = startResponse() }
|
||||
|
||||
override DataFlow::Node getBulkArg() {
|
||||
result = [this.getArg(1), this.getArgByName("headers")]
|
||||
}
|
||||
|
||||
// TODO: These checks perhaps could be made more precise.
|
||||
override predicate nameAllowsNewline() { not existsValidatedApplication() }
|
||||
|
||||
override predicate valueAllowsNewline() { not existsValidatedApplication() }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -12,6 +12,7 @@ private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
|
||||
/**
|
||||
* Provides models for the `Werkzeug` PyPI package.
|
||||
@@ -144,6 +145,18 @@ module Werkzeug {
|
||||
* See https://werkzeug.palletsprojects.com/en/1.0.x/datastructures/#werkzeug.datastructures.Headers.
|
||||
*/
|
||||
module Headers {
|
||||
/** Gets a reference to the `werkzeug.datastructures.Headers` class. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("werkzeug").getMember("datastructures").getMember("Headers")
|
||||
or
|
||||
result = ModelOutput::getATypeNode("werkzeug.datastructures.Headers~Subclass").getASubclass*()
|
||||
}
|
||||
|
||||
/** A direct instantiation of `werkzeug.datastructures.Headers`. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `werkzeug.datastructures.Headers`, extend this class to model new instances.
|
||||
*
|
||||
@@ -182,6 +195,61 @@ module Werkzeug {
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** A call to a method that writes to a header, assumed to be a response header. */
|
||||
private class HeaderWriteCall extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
HeaderWriteCall() {
|
||||
this.calls(instance(), ["add", "add_header", "set", "setdefault", "__setitem__"])
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = this.getArg(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.getArg(1) }
|
||||
|
||||
override predicate nameAllowsNewline() { any() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
|
||||
/** A dict-like write to a header, assumed to be a response header. */
|
||||
private class HeaderWriteSubscript extends Http::Server::ResponseHeaderWrite::Range,
|
||||
DataFlow::Node
|
||||
{
|
||||
DataFlow::Node name;
|
||||
DataFlow::Node value;
|
||||
|
||||
HeaderWriteSubscript() {
|
||||
exists(SubscriptNode subscript |
|
||||
this.asCfgNode() = subscript and
|
||||
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
|
||||
name.asCfgNode() = subscript.getIndex() and
|
||||
subscript.getObject() = instance().asCfgNode()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = name }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = value }
|
||||
|
||||
override predicate nameAllowsNewline() { any() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
|
||||
/** A call to `Headers.extend`, assumed to be a response header. */
|
||||
private class HeaderExtendCall extends Http::Server::ResponseHeaderBulkWrite::Range,
|
||||
DataFlow::MethodCallNode
|
||||
{
|
||||
HeaderExtendCall() { this.calls(instance(), "extend") }
|
||||
|
||||
override DataFlow::Node getBulkArg() { result = this.getArg(0) }
|
||||
|
||||
override predicate nameAllowsNewline() { any() }
|
||||
|
||||
override predicate valueAllowsNewline() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extensions:
|
||||
# Contribute empty data sets to avoid errors about an undefined extensionals
|
||||
# Make sure that the extensible model predicates have at least one definition
|
||||
# to avoid errors about undefined extensionals.
|
||||
- addsTo:
|
||||
pack: codeql/python-all
|
||||
extensible: sourceModel
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
/**
|
||||
* Provides default sources, sinks, and sanitizers for detecting
|
||||
* "HTTP Header injection" vulnerabilities, as well as extension
|
||||
* points for adding your own.
|
||||
*/
|
||||
|
||||
import python
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* Provides default sources, sinks, and sanitizers for detecting
|
||||
* "HTTP Header injection" vulnerabilities, as well as extension
|
||||
* points for adding your own.
|
||||
*/
|
||||
module HttpHeaderInjection {
|
||||
/**
|
||||
* A data flow source for "HTTP Header injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Source extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sink for "HTTP Header injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Sink extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A data flow sanitizer for "HTTP Header injection" vulnerabilities.
|
||||
*/
|
||||
abstract class Sanitizer extends DataFlow::Node { }
|
||||
|
||||
/**
|
||||
* A source of remote user input, considered as a flow source.
|
||||
*/
|
||||
class RemoteFlowSourceAsSource extends Source, RemoteFlowSource { }
|
||||
|
||||
/**
|
||||
* A HTTP header write, considered as a flow sink.
|
||||
*/
|
||||
class HeaderWriteAsSink extends Sink {
|
||||
HeaderWriteAsSink() {
|
||||
exists(Http::Server::ResponseHeaderWrite headerWrite |
|
||||
headerWrite.nameAllowsNewline() and
|
||||
this = headerWrite.getNameArg()
|
||||
or
|
||||
headerWrite.valueAllowsNewline() and
|
||||
this = headerWrite.getValueArg()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** A key-value pair in a literal for a bulk header update, considered as a single header update. */
|
||||
// TODO: We could instead consider bulk writes as sinks with an implicit read step of DictionaryKey/DictionaryValue content as needed.
|
||||
private class HeaderBulkWriteDictLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
|
||||
{
|
||||
KeyValuePair item;
|
||||
|
||||
HeaderBulkWriteDictLiteral() {
|
||||
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.getBulkArg()) |
|
||||
item = dict.getAnItem()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
|
||||
|
||||
override predicate nameAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
|
||||
}
|
||||
|
||||
override predicate valueAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
|
||||
}
|
||||
}
|
||||
|
||||
/** A tuple in a list for a bulk header update, considered as a single header update. */
|
||||
// TODO: We could instead consider bulk writes as sinks with implicit read steps as needed.
|
||||
private class HeaderBulkWriteListLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
|
||||
{
|
||||
Tuple item;
|
||||
|
||||
HeaderBulkWriteListLiteral() {
|
||||
exists(List list | DataFlow::localFlow(DataFlow::exprNode(list), super.getBulkArg()) |
|
||||
item = list.getAnElt()
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getElt(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getElt(1) }
|
||||
|
||||
override predicate nameAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
|
||||
}
|
||||
|
||||
override predicate valueAllowsNewline() {
|
||||
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A call to replace line breaks, considered as a sanitizer.
|
||||
*/
|
||||
class ReplaceLineBreaksSanitizer extends Sanitizer, DataFlow::CallCfgNode {
|
||||
ReplaceLineBreaksSanitizer() {
|
||||
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "replace" and
|
||||
this.getArg(0).asExpr().(StringLiteral).getText() = "\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
/**
|
||||
* Provides a taint tracking configuration for reasoning about HTTP header injection.
|
||||
*/
|
||||
|
||||
import python
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import HttpHeaderInjectionCustomizations
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting HTTP Header injection vulnerabilities.
|
||||
*/
|
||||
private module HeaderInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node node) { node instanceof HttpHeaderInjection::Source }
|
||||
|
||||
predicate isSink(DataFlow::Node node) { node instanceof HttpHeaderInjection::Sink }
|
||||
|
||||
predicate isBarrier(DataFlow::Node node) { node instanceof HttpHeaderInjection::Sanitizer }
|
||||
}
|
||||
|
||||
/** Global taint-tracking for detecting "HTTP Header injection" vulnerabilities. */
|
||||
module HeaderInjectionFlow = TaintTracking::Global<HeaderInjectionConfig>;
|
||||
@@ -1,4 +1,4 @@
|
||||
description: Removed unused column from the `folders` and `files` relations
|
||||
compatibility: full
|
||||
files.rel: reorder files.rel (int id, string name, string simple, string ext, int fromSource) id name
|
||||
folders.rel: reorder folders.rel (int id, string name, string simple) id name
|
||||
files.rel: reorder files.rel (@file id, string name, string simple, string ext, int fromSource) id name
|
||||
folders.rel: reorder folders.rel (@folder id, string name, string simple) id name
|
||||
@@ -1,3 +1,7 @@
|
||||
## 0.9.15
|
||||
|
||||
No user-facing changes.
|
||||
|
||||
## 0.9.14
|
||||
|
||||
No user-facing changes.
|
||||
@@ -277,7 +281,7 @@ No user-facing changes.
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* The [View AST functionality](https://codeql.github.com/docs/codeql-for-visual-studio-code/exploring-the-structure-of-your-source-code/) no longer prints detailed information about regular expressions, greatly improving performance.
|
||||
* The [View AST functionality](https://docs.github.com/en/code-security/codeql-for-vs-code/using-the-advanced-functionality-of-the-codeql-for-vs-code-extension/exploring-the-structure-of-your-source-code) no longer prints detailed information about regular expressions, greatly improving performance.
|
||||
|
||||
## 0.0.8
|
||||
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
/**
|
||||
* Provides predicates for reasoning about regular expressions
|
||||
* that match URLs and hostname patterns.
|
||||
*/
|
||||
|
||||
// HostnameRegexp should be used directly from the shared regex pack, and not from this file.
|
||||
deprecated private import semmle.python.security.regexp.HostnameRegex as Dep
|
||||
import Dep
|
||||
41
python/ql/src/Security/CWE-113/HeaderInjection.qhelp
Normal file
41
python/ql/src/Security/CWE-113/HeaderInjection.qhelp
Normal file
@@ -0,0 +1,41 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
|
||||
<overview>
|
||||
<p>Directly writing user input (for example, an HTTP request parameter) to an HTTP header
|
||||
can lead to an HTTP response-splitting vulnerability.</p>
|
||||
|
||||
<p>If user-controlled input is used in an HTTP header that allows line break characters, an attacker can
|
||||
inject additional headers or control the response body, leading to vulnerabilities such as XSS or cache poisoning.
|
||||
</p>
|
||||
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>Ensure that user input containing line break characters is not written to an HTTP header.</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>In the following example, the case marked BAD writes user input to the header name.
|
||||
In the GOOD case, input is first escaped to not contain any line break characters.</p>
|
||||
<sample src="examples/header_injection.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>
|
||||
SecLists.org: <a href="https://seclists.org/bugtraq/2005/Apr/187">HTTP response splitting</a>.
|
||||
</li>
|
||||
<li>
|
||||
OWASP:
|
||||
<a href="https://www.owasp.org/index.php/HTTP_Response_Splitting">HTTP Response Splitting</a>.
|
||||
</li>
|
||||
<li>
|
||||
Wikipedia: <a href="http://en.wikipedia.org/wiki/HTTP_response_splitting">HTTP response splitting</a>.
|
||||
</li>
|
||||
<li>
|
||||
CAPEC: <a href="https://capec.mitre.org/data/definitions/105.html">CAPEC-105: HTTP Request Splitting</a>
|
||||
</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* @name HTTP Header Injection
|
||||
* @description User input should not be used in HTTP headers, otherwise a malicious user
|
||||
* may be able to inject a value that could manipulate the response.
|
||||
* @name HTTP Response Splitting
|
||||
* @description Writing user input directly to an HTTP header
|
||||
* makes code vulnerable to attack by header splitting.
|
||||
* @kind path-problem
|
||||
* @problem.severity error
|
||||
* @id py/header-injection
|
||||
* @security-severity 6.1
|
||||
* @precision high
|
||||
* @id py/http-response-splitting
|
||||
* @tags security
|
||||
* experimental
|
||||
* external/cwe/cwe-113
|
||||
* external/cwe/cwe-079
|
||||
*/
|
||||
|
||||
// determine precision above
|
||||
import python
|
||||
import experimental.semmle.python.security.injection.HTTPHeaders
|
||||
import semmle.python.security.dataflow.HttpHeaderInjectionQuery
|
||||
import HeaderInjectionFlow::PathGraph
|
||||
|
||||
from HeaderInjectionFlow::PathNode source, HeaderInjectionFlow::PathNode sink
|
||||
17
python/ql/src/Security/CWE-113/examples/header_injection.py
Normal file
17
python/ql/src/Security/CWE-113/examples/header_injection.py
Normal file
@@ -0,0 +1,17 @@
|
||||
@app.route("/example_bad")
|
||||
def example_bad():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
custom_header = "X-MyHeader-" + rfs_header
|
||||
# BAD: User input is used as part of the header name.
|
||||
response.headers[custom_header] = "HeaderValue"
|
||||
return response
|
||||
|
||||
@app.route("/example_good")
|
||||
def example_bad():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
custom_header = "X-MyHeader-" + rfs_header.replace("\n", "").replace("\r","").replace(":","")
|
||||
# GOOD: Line break characters are removed from the input.
|
||||
response.headers[custom_header] = "HeaderValue"
|
||||
return response
|
||||
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: newQuery
|
||||
---
|
||||
* The `py/header-injection` query, originally contributed to the experimental query pack by @jorgectf, has been promoted to the main query pack and renamed to `py/http-response-splitting`. This query finds instances of http header injection / response splitting vulnerabilities.
|
||||
@@ -2,4 +2,4 @@
|
||||
|
||||
### Bug Fixes
|
||||
|
||||
* The [View AST functionality](https://codeql.github.com/docs/codeql-for-visual-studio-code/exploring-the-structure-of-your-source-code/) no longer prints detailed information about regular expressions, greatly improving performance.
|
||||
* The [View AST functionality](https://docs.github.com/en/code-security/codeql-for-vs-code/using-the-advanced-functionality-of-the-codeql-for-vs-code-extension/exploring-the-structure-of-your-source-code) no longer prints detailed information about regular expressions, greatly improving performance.
|
||||
|
||||
3
python/ql/src/change-notes/released/0.9.15.md
Normal file
3
python/ql/src/change-notes/released/0.9.15.md
Normal file
@@ -0,0 +1,3 @@
|
||||
## 0.9.15
|
||||
|
||||
No user-facing changes.
|
||||
@@ -1,2 +1,2 @@
|
||||
---
|
||||
lastReleaseVersion: 0.9.14
|
||||
lastReleaseVersion: 0.9.15
|
||||
|
||||
@@ -1,26 +0,0 @@
|
||||
<!DOCTYPE qhelp PUBLIC
|
||||
"-//Semmle//qhelp//EN"
|
||||
"qhelp.dtd">
|
||||
<qhelp>
|
||||
<overview>
|
||||
<p>If an HTTP Header is built using string concatenation or string formatting, and the
|
||||
components of the concatenation include user input, a user
|
||||
is likely to be able to manipulate the response.</p>
|
||||
</overview>
|
||||
|
||||
<recommendation>
|
||||
<p>User input should not be included in an HTTP Header.</p>
|
||||
</recommendation>
|
||||
|
||||
<example>
|
||||
<p>In the following example, the code appends a user-provided value into a header.</p>
|
||||
|
||||
<sample src="header_injection.py" />
|
||||
</example>
|
||||
|
||||
<references>
|
||||
<li>OWASP: <a href="https://owasp.org/www-community/attacks/HTTP_Response_Splitting">HTTP Response Splitting</a>.</li>
|
||||
<li>Python Security: <a href="https://python-security.readthedocs.io/vuln/http-header-injection.html">HTTP header injection</a>.</li>
|
||||
<li>SonarSource: <a href="https://rules.sonarsource.com/python/RSPEC-5167">RSPEC-5167</a>.</li>
|
||||
</references>
|
||||
</qhelp>
|
||||
@@ -1,9 +0,0 @@
|
||||
from flask import Response, request, Flask, make_response
|
||||
|
||||
|
||||
@app.route("/flask_Response")
|
||||
def flask_Response():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
response.headers['HeaderName'] = rfs_header
|
||||
return response
|
||||
@@ -216,45 +216,6 @@ class SqlEscape extends DataFlow::Node instanceof SqlEscape::Range {
|
||||
DataFlow::Node getAnInput() { result = super.getAnInput() }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling HTTP Header APIs. */
|
||||
module HeaderDeclaration {
|
||||
/**
|
||||
* A data-flow node that collects functions setting HTTP Headers.
|
||||
*
|
||||
* Extend this class to model new APIs. If you want to refine existing API models,
|
||||
* extend `HeaderDeclaration` instead.
|
||||
*/
|
||||
abstract class Range extends DataFlow::Node {
|
||||
/**
|
||||
* Gets the argument containing the header name.
|
||||
*/
|
||||
abstract DataFlow::Node getNameArg();
|
||||
|
||||
/**
|
||||
* Gets the argument containing the header value.
|
||||
*/
|
||||
abstract DataFlow::Node getValueArg();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A data-flow node that collects functions setting HTTP Headers.
|
||||
*
|
||||
* Extend this class to refine existing API models. If you want to model new APIs,
|
||||
* extend `HeaderDeclaration::Range` instead.
|
||||
*/
|
||||
class HeaderDeclaration extends DataFlow::Node instanceof HeaderDeclaration::Range {
|
||||
/**
|
||||
* Gets the argument containing the header name.
|
||||
*/
|
||||
DataFlow::Node getNameArg() { result = super.getNameArg() }
|
||||
|
||||
/**
|
||||
* Gets the argument containing the header value.
|
||||
*/
|
||||
DataFlow::Node getValueArg() { result = super.getValueArg() }
|
||||
}
|
||||
|
||||
/** Provides classes for modeling Csv writer APIs. */
|
||||
module CsvWriter {
|
||||
/**
|
||||
|
||||
@@ -6,6 +6,7 @@ import python
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.Concepts
|
||||
|
||||
/**
|
||||
* Gets a header setting a cookie.
|
||||
@@ -26,13 +27,13 @@ import experimental.semmle.python.Concepts
|
||||
* * `isSameSite()` predicate would fail.
|
||||
* * `getName()` and `getValue()` results would be `"name=value; Secure;"`.
|
||||
*/
|
||||
class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
|
||||
class CookieHeader extends Cookie::Range instanceof Http::Server::ResponseHeaderWrite {
|
||||
CookieHeader() {
|
||||
exists(StringLiteral str |
|
||||
str.getText() = "Set-Cookie" and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getNameArg())
|
||||
.flowsTo(this.(Http::Server::ResponseHeaderWrite).getNameArg())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -41,7 +42,7 @@ class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
|
||||
str.getText().regexpMatch(".*; *Secure;.*") and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getValueArg())
|
||||
.flowsTo(this.(Http::Server::ResponseHeaderWrite).getValueArg())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -50,7 +51,7 @@ class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
|
||||
str.getText().regexpMatch(".*; *HttpOnly;.*") and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getValueArg())
|
||||
.flowsTo(this.(Http::Server::ResponseHeaderWrite).getValueArg())
|
||||
)
|
||||
}
|
||||
|
||||
@@ -59,13 +60,17 @@ class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
|
||||
str.getText().regexpMatch(".*; *SameSite=(Strict|Lax);.*") and
|
||||
DataFlow::exprNode(str)
|
||||
.(DataFlow::LocalSourceNode)
|
||||
.flowsTo(this.(HeaderDeclaration).getValueArg())
|
||||
.flowsTo(this.(Http::Server::ResponseHeaderWrite).getValueArg())
|
||||
)
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = this.(HeaderDeclaration).getValueArg() }
|
||||
override DataFlow::Node getNameArg() {
|
||||
result = this.(Http::Server::ResponseHeaderWrite).getValueArg()
|
||||
}
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.(HeaderDeclaration).getValueArg() }
|
||||
override DataFlow::Node getValueArg() {
|
||||
result = this.(Http::Server::ResponseHeaderWrite).getValueArg()
|
||||
}
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
private import experimental.semmle.python.frameworks.Stdlib
|
||||
private import experimental.semmle.python.frameworks.Flask
|
||||
private import experimental.semmle.python.frameworks.Django
|
||||
private import experimental.semmle.python.frameworks.Werkzeug
|
||||
private import experimental.semmle.python.frameworks.LDAP
|
||||
private import experimental.semmle.python.frameworks.JWT
|
||||
private import experimental.semmle.python.frameworks.Csv
|
||||
|
||||
@@ -88,31 +88,6 @@ private module ExperimentalPrivateDjango {
|
||||
result = baseClassRef().getReturn().getAMember()
|
||||
}
|
||||
|
||||
class DjangoResponseSetItemCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
|
||||
DjangoResponseSetItemCall() {
|
||||
this = baseClassRef().getReturn().getMember("__setitem__").getACall()
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = this.getArg(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.getArg(1) }
|
||||
}
|
||||
|
||||
class DjangoResponseDefinition extends DataFlow::Node, HeaderDeclaration::Range {
|
||||
DataFlow::Node headerInput;
|
||||
|
||||
DjangoResponseDefinition() {
|
||||
headerInput = headerInstance().asSink() and
|
||||
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() {
|
||||
result.asExpr() = this.asExpr().(Subscript).getIndex()
|
||||
}
|
||||
|
||||
override DataFlow::Node getValueArg() { result = headerInput }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `set_cookie()`.
|
||||
*
|
||||
|
||||
@@ -11,76 +11,6 @@ private import semmle.python.ApiGraphs
|
||||
private import semmle.python.frameworks.Flask
|
||||
|
||||
module ExperimentalFlask {
|
||||
/**
|
||||
* A reference to either `flask.make_response` function, or the `make_response` method on
|
||||
* an instance of `flask.Flask`. This creates an instance of the `flask_response`
|
||||
* class (class-attribute on a flask application), which by default is
|
||||
* `flask.Response`.
|
||||
*
|
||||
* See
|
||||
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.make_response
|
||||
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response
|
||||
*/
|
||||
private API::Node flaskMakeResponse() {
|
||||
result =
|
||||
[API::moduleImport("flask"), Flask::FlaskApp::instance()]
|
||||
.getMember(["make_response", "jsonify", "make_default_options_response"])
|
||||
}
|
||||
|
||||
/** Gets a reference to a header instance. */
|
||||
private DataFlow::LocalSourceNode headerInstance() {
|
||||
result =
|
||||
[Flask::Response::classRef(), flaskMakeResponse()]
|
||||
.getReturn()
|
||||
.getAMember()
|
||||
.getAValueReachableFromSource()
|
||||
}
|
||||
|
||||
/** Gets a reference to a header instance call/subscript */
|
||||
private DataFlow::Node headerInstanceCall() {
|
||||
headerInstance() in [result.(DataFlow::AttrRead), result.(DataFlow::AttrRead).getObject()] or
|
||||
headerInstance().asExpr() = result.asExpr().(Subscript).getObject()
|
||||
}
|
||||
|
||||
class FlaskHeaderDefinition extends DataFlow::Node, HeaderDeclaration::Range {
|
||||
DataFlow::Node headerInput;
|
||||
|
||||
FlaskHeaderDefinition() {
|
||||
this.asCfgNode().(DefinitionNode) = headerInstanceCall().asCfgNode() and
|
||||
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = this.asExpr().(Subscript).getIndex() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = headerInput }
|
||||
}
|
||||
|
||||
private class FlaskMakeResponseExtend extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
|
||||
KeyValuePair item;
|
||||
|
||||
FlaskMakeResponseExtend() {
|
||||
this.getFunction() = headerInstanceCall() and
|
||||
item = this.getArg(_).asExpr().(Dict).getAnItem()
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
|
||||
}
|
||||
|
||||
private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
|
||||
KeyValuePair item;
|
||||
|
||||
FlaskResponse() {
|
||||
this = Flask::Response::classRef().getACall() and
|
||||
item = this.getArg(_).asExpr().(Dict).getAnItem()
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
|
||||
|
||||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets a call to `set_cookie()`.
|
||||
*
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `Werkzeug` PyPI package.
|
||||
* See
|
||||
* - https://pypi.org/project/Werkzeug/
|
||||
* - https://werkzeug.palletsprojects.com/en/1.0.x/#werkzeug
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.frameworks.Flask
|
||||
private import semmle.python.dataflow.new.DataFlow
|
||||
private import experimental.semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
|
||||
private module Werkzeug {
|
||||
module Datastructures {
|
||||
module Headers {
|
||||
class WerkzeugHeaderAddCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
|
||||
WerkzeugHeaderAddCall() {
|
||||
this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() =
|
||||
API::moduleImport("werkzeug")
|
||||
.getMember("datastructures")
|
||||
.getMember("Headers")
|
||||
.getACall() and
|
||||
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "add"
|
||||
}
|
||||
|
||||
override DataFlow::Node getNameArg() { result = this.getArg(0) }
|
||||
|
||||
override DataFlow::Node getValueArg() { result = this.getArg(1) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
import python
|
||||
import experimental.semmle.python.Concepts
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.dataflow.new.TaintTracking
|
||||
import semmle.python.dataflow.new.RemoteFlowSources
|
||||
|
||||
/**
|
||||
* A taint-tracking configuration for detecting HTTP Header injections.
|
||||
*/
|
||||
private module HeaderInjectionConfig implements DataFlow::ConfigSig {
|
||||
predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
|
||||
|
||||
predicate isSink(DataFlow::Node sink) {
|
||||
exists(HeaderDeclaration headerDeclaration |
|
||||
sink in [headerDeclaration.getNameArg(), headerDeclaration.getValueArg()]
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Global taint-tracking for detecting "HTTP Header injection" vulnerabilities. */
|
||||
module HeaderInjectionFlow = TaintTracking::Global<HeaderInjectionConfig>;
|
||||
@@ -1,5 +1,5 @@
|
||||
name: codeql/python-queries
|
||||
version: 0.9.15-dev
|
||||
version: 0.9.16-dev
|
||||
groups:
|
||||
- python
|
||||
- queries
|
||||
|
||||
@@ -319,6 +319,66 @@ module HttpServerHttpResponseTest implements TestSig {
|
||||
}
|
||||
}
|
||||
|
||||
module HttpResponseHeaderWriteTest implements TestSig {
|
||||
string getARelevantTag() {
|
||||
result =
|
||||
[
|
||||
"headerWriteNameUnsanitized", "headerWriteNameSanitized", "headerWriteValueUnsanitized",
|
||||
"headerWriteValueSanitized", "headerWriteBulk"
|
||||
]
|
||||
}
|
||||
|
||||
predicate hasActualResult(Location location, string element, string tag, string value) {
|
||||
exists(location.getFile().getRelativePath()) and
|
||||
(
|
||||
exists(Http::Server::ResponseHeaderWrite write, DataFlow::Node node |
|
||||
location = node.getLocation() and
|
||||
element = node.toString()
|
||||
|
|
||||
node = write.getNameArg() and
|
||||
(
|
||||
if write.nameAllowsNewline()
|
||||
then tag = "headerWriteNameUnsanitized"
|
||||
else tag = "headerWriteNameSanitized"
|
||||
) and
|
||||
value = prettyNodeForInlineTest(node)
|
||||
or
|
||||
node = write.getValueArg() and
|
||||
(
|
||||
if write.valueAllowsNewline()
|
||||
then tag = "headerWriteValueUnsanitized"
|
||||
else tag = "headerWriteValueSanitized"
|
||||
) and
|
||||
value = prettyNodeForInlineTest(node)
|
||||
)
|
||||
or
|
||||
exists(Http::Server::ResponseHeaderBulkWrite write, DataFlow::Node node |
|
||||
node = write.getBulkArg() and
|
||||
location = node.getLocation() and
|
||||
element = node.toString() and
|
||||
(
|
||||
tag = "headerWriteBulk" and
|
||||
value = prettyNodeForInlineTest(node)
|
||||
or
|
||||
(
|
||||
if write.nameAllowsNewline()
|
||||
then tag = "headerWriteNameUnsanitized"
|
||||
else tag = "headerWriteNameSanitized"
|
||||
) and
|
||||
value = ""
|
||||
or
|
||||
(
|
||||
if write.valueAllowsNewline()
|
||||
then tag = "headerWriteValueUnsanitized"
|
||||
else tag = "headerWriteValueSanitized"
|
||||
) and
|
||||
value = ""
|
||||
)
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
module HttpServerHttpRedirectResponseTest implements TestSig {
|
||||
string getARelevantTag() { result in ["HttpRedirectResponse", "redirectLocation"] }
|
||||
|
||||
@@ -559,7 +619,8 @@ import MakeTest<MergeTests5<MergeTests5<SystemCommandExecutionTest, DecodingTest
|
||||
MergeTests5<SqlConstructionTest, SqlExecutionTest, XPathConstructionTest, XPathExecutionTest,
|
||||
EscapingTest>,
|
||||
MergeTests5<HttpServerRouteSetupTest, HttpServerRequestHandlerTest, HttpServerHttpResponseTest,
|
||||
HttpServerHttpRedirectResponseTest, HttpServerCookieWriteTest>,
|
||||
HttpServerHttpRedirectResponseTest,
|
||||
MergeTests<HttpServerCookieWriteTest, HttpResponseHeaderWriteTest>>,
|
||||
MergeTests5<FileSystemAccessTest, FileSystemWriteAccessTest, PathNormalizationTest,
|
||||
SafeAccessCheckTest, PublicKeyGenerationTest>,
|
||||
MergeTests5<CryptographicOperationTest, HttpClientRequestTest, CsrfProtectionSettingTest,
|
||||
|
||||
@@ -1,47 +0,0 @@
|
||||
edges
|
||||
| django_bad.py:5:5:5:14 | ControlFlowNode for rfs_header | django_bad.py:7:40:7:49 | ControlFlowNode for rfs_header | provenance | |
|
||||
| django_bad.py:5:18:5:58 | ControlFlowNode for Attribute() | django_bad.py:5:5:5:14 | ControlFlowNode for rfs_header | provenance | |
|
||||
| django_bad.py:12:5:12:14 | ControlFlowNode for rfs_header | django_bad.py:14:30:14:39 | ControlFlowNode for rfs_header | provenance | |
|
||||
| django_bad.py:12:18:12:58 | ControlFlowNode for Attribute() | django_bad.py:12:5:12:14 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_bad.py:1:29:1:35 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for request | flask_bad.py:9:18:9:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for request | flask_bad.py:19:18:19:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for request | flask_bad.py:27:18:27:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for request | flask_bad.py:35:18:35:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:9:5:9:14 | ControlFlowNode for rfs_header | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_bad.py:9:18:9:24 | ControlFlowNode for request | flask_bad.py:9:5:9:14 | ControlFlowNode for rfs_header | provenance | AdditionalTaintStep |
|
||||
| flask_bad.py:19:5:19:14 | ControlFlowNode for rfs_header | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_bad.py:19:18:19:24 | ControlFlowNode for request | flask_bad.py:19:5:19:14 | ControlFlowNode for rfs_header | provenance | AdditionalTaintStep |
|
||||
| flask_bad.py:27:5:27:14 | ControlFlowNode for rfs_header | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_bad.py:27:18:27:24 | ControlFlowNode for request | flask_bad.py:27:5:27:14 | ControlFlowNode for rfs_header | provenance | AdditionalTaintStep |
|
||||
| flask_bad.py:35:5:35:14 | ControlFlowNode for rfs_header | flask_bad.py:38:24:38:33 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_bad.py:35:18:35:24 | ControlFlowNode for request | flask_bad.py:35:5:35:14 | ControlFlowNode for rfs_header | provenance | AdditionalTaintStep |
|
||||
nodes
|
||||
| django_bad.py:5:5:5:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| django_bad.py:5:18:5:58 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:7:40:7:49 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| django_bad.py:12:5:12:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| django_bad.py:12:18:12:58 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:14:30:14:39 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
|
||||
| flask_bad.py:1:29:1:35 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:9:5:9:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:9:18:9:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:19:5:19:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:19:18:19:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:27:5:27:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:27:18:27:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:35:5:35:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_bad.py:35:18:35:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:38:24:38:33 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
subpaths
|
||||
#select
|
||||
| django_bad.py:7:40:7:49 | ControlFlowNode for rfs_header | django_bad.py:5:18:5:58 | ControlFlowNode for Attribute() | django_bad.py:7:40:7:49 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | django_bad.py:5:18:5:58 | ControlFlowNode for Attribute() | user-provided value |
|
||||
| django_bad.py:14:30:14:39 | ControlFlowNode for rfs_header | django_bad.py:12:18:12:58 | ControlFlowNode for Attribute() | django_bad.py:14:30:14:39 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | django_bad.py:12:18:12:58 | ControlFlowNode for Attribute() | user-provided value |
|
||||
| flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| flask_bad.py:38:24:38:33 | ControlFlowNode for rfs_header | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_bad.py:38:24:38:33 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_bad.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
@@ -1 +0,0 @@
|
||||
experimental/Security/CWE-113/HeaderInjection.ql
|
||||
@@ -1,15 +0,0 @@
|
||||
import django.http
|
||||
|
||||
|
||||
def django_setitem():
|
||||
rfs_header = django.http.request.GET.get("rfs_header")
|
||||
response = django.http.HttpResponse()
|
||||
response.__setitem__('HeaderName', rfs_header)
|
||||
return response
|
||||
|
||||
|
||||
def django_response():
|
||||
rfs_header = django.http.request.GET.get("rfs_header")
|
||||
response = django.http.HttpResponse()
|
||||
response['HeaderName'] = rfs_header
|
||||
return response
|
||||
@@ -1,47 +0,0 @@
|
||||
from flask import Response, request, Flask, make_response
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/werkzeug_headers')
|
||||
def werkzeug_headers():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
headers = Headers()
|
||||
headers.add("HeaderName", rfs_header)
|
||||
response.headers = headers
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/flask_Response")
|
||||
def flask_Response():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
response.headers['HeaderName'] = rfs_header
|
||||
return response
|
||||
|
||||
|
||||
@app.route("/flask_make_response")
|
||||
def flask_make_response():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
resp = make_response("hello")
|
||||
resp.headers['HeaderName'] = rfs_header
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/flask_make_response_extend")
|
||||
def flask_make_response_extend():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
resp = make_response("hello")
|
||||
resp.headers.extend(
|
||||
{'HeaderName': rfs_header})
|
||||
return resp
|
||||
|
||||
|
||||
@app.route("/Response_arg")
|
||||
def Response_arg():
|
||||
return Response(headers={'HeaderName': request.args["rfs_header"]})
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app.run(debug=True)
|
||||
@@ -1,6 +1,4 @@
|
||||
edges
|
||||
| django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | provenance | |
|
||||
| django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | provenance | |
|
||||
| flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_bad.py:1:26:1:32 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:1:26:1:32 | ControlFlowNode for request | flask_bad.py:24:21:24:27 | ControlFlowNode for request | provenance | |
|
||||
| flask_bad.py:1:26:1:32 | ControlFlowNode for request | flask_bad.py:24:49:24:55 | ControlFlowNode for request | provenance | |
|
||||
@@ -14,9 +12,6 @@ edges
|
||||
nodes
|
||||
| django_bad.py:19:21:19:55 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | semmle.label | ControlFlowNode for Fstring |
|
||||
| django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | semmle.label | ControlFlowNode for Attribute() |
|
||||
| flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
|
||||
| flask_bad.py:1:26:1:32 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_bad.py:24:21:24:27 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
@@ -34,12 +29,6 @@ subpaths
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:20:21:20:56 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:27:33:27:67 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its httponly flag is not properly set. | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its samesite flag is not properly set. | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | django_bad.py:27:30:27:124 | ControlFlowNode for Fstring | Cookie is constructed from a $@,and its secure flag is not properly set. | django_bad.py:27:71:27:106 | ControlFlowNode for Attribute() | user-supplied input |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its httponly flag is not properly set. | flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | user-supplied input |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its samesite flag is not properly set. | flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | user-supplied input |
|
||||
| flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | flask_bad.py:24:21:24:40 | ControlFlowNode for Subscript | Cookie is constructed from a $@,and its secure flag is not properly set. | flask_bad.py:1:26:1:32 | ControlFlowNode for ImportMember | user-supplied input |
|
||||
|
||||
@@ -1,15 +1,9 @@
|
||||
| django_bad.py:6:5:7:52 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:6:5:7:52 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:6:5:7:52 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_bad.py:13:5:13:26 | ControlFlowNode for Subscript | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:13:5:13:26 | ControlFlowNode for Subscript | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:13:5:13:26 | ControlFlowNode for Subscript | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_bad.py:19:5:21:66 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:19:5:21:66 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:19:5:21:66 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_bad.py:27:5:27:26 | ControlFlowNode for Subscript | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_bad.py:27:5:27:26 | ControlFlowNode for Subscript | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_bad.py:27:5:27:26 | ControlFlowNode for Subscript | Cookie is added without the 'secure' flag properly set. |
|
||||
| django_good.py:19:5:19:44 | ControlFlowNode for Attribute() | Cookie is added without the 'httponly' flag properly set. |
|
||||
| django_good.py:19:5:19:44 | ControlFlowNode for Attribute() | Cookie is added without the 'samesite' flag properly set. |
|
||||
| django_good.py:19:5:19:44 | ControlFlowNode for Attribute() | Cookie is added without the 'secure' flag properly set. |
|
||||
|
||||
@@ -7,7 +7,7 @@ def django_response(request):
|
||||
httponly=False, samesite='None')
|
||||
return resp
|
||||
|
||||
|
||||
# This test no longer produces an output due to django header setting methods not being modeled in the main query pack
|
||||
def django_response():
|
||||
response = django.http.HttpResponse()
|
||||
response['Set-Cookie'] = "name=value; SameSite=None;"
|
||||
@@ -21,7 +21,7 @@ def django_response(request):
|
||||
secure=False, httponly=False, samesite='None')
|
||||
return resp
|
||||
|
||||
|
||||
# This test no longer produces an output due to django header setting methods not being modeled in the main query pack
|
||||
def django_response():
|
||||
response = django.http.HttpResponse()
|
||||
response['Set-Cookie'] = f"{django.http.request.GET.get('name')}={django.http.request.GET.get('value')}; SameSite=None;"
|
||||
|
||||
@@ -20,4 +20,7 @@ extensions:
|
||||
- ["foo", "Member[MS_spread_all]", "Argument[0]", "ReturnValue.TupleElement[0,1]", "value"]
|
||||
- ["foo", "Member[MS_Class].Call", "Argument[0, x:]", "ReturnValue.Attribute[config]", "value"]
|
||||
- ["foo", "Member[MS_Class_transitive].Subclass.Call", "Argument[0, x:]", "ReturnValue.Attribute[config]", "value"]
|
||||
- ["foo", "Member[MS_Class].Instance.Member[instance_method]", "Argument[self]", "ReturnValue.TupleElement[0]", "value"]
|
||||
- ["foo", "Member[MS_Class].Instance.Member[instance_method]", "Argument[0]", "ReturnValue.TupleElement[1]", "value"]
|
||||
- ["foo", "Member[MS_Class].Instance.Member[explicit_self]", "Argument[self:]", "ReturnValue", "value"]
|
||||
- ["json", "Member[MS_loads]", "Argument[0]", "ReturnValue", "taint"]
|
||||
|
||||
@@ -20,4 +20,7 @@ extensions:
|
||||
- ["foo", "Member[MS_spread_all]", "Argument[0]", "ReturnValue.TupleElement[0,1]", "value"]
|
||||
- ["foo", "Member[MS_Class].Call", "Argument[0, x:]", "ReturnValue.Attribute[config]", "value"]
|
||||
- ["foo", "Member[MS_Class_transitive].Subclass.Call", "Argument[0, x:]", "ReturnValue.Attribute[config]", "value"]
|
||||
- ["foo", "Member[MS_Class].Instance.Member[instance_method]", "Argument[self]", "ReturnValue.TupleElement[0]", "value"]
|
||||
- ["foo", "Member[MS_Class].Instance.Member[instance_method]", "Argument[0]", "ReturnValue.TupleElement[1]", "value"]
|
||||
- ["foo", "Member[MS_Class].Instance.Member[explicit_self]", "Argument[self:]", "ReturnValue", "value"]
|
||||
- ["json", "Member[MS_loads]", "Argument[0]", "ReturnValue", "taint"]
|
||||
|
||||
@@ -157,6 +157,21 @@ class E(MS_Class_transitive):
|
||||
SINK(E(NONSOURCE, SOURCE).config) # $ MISSING: flow="SOURCE -> E(..).config"
|
||||
SINK(E(x = SOURCE, y = NONSOURCE).config) # $ flow="SOURCE -> E(..).config"
|
||||
|
||||
c = MS_Class()
|
||||
a, b = c.instance_method(SOURCE)
|
||||
SINK_F(a)
|
||||
SINK(b) # $ flow="SOURCE, l:-2 -> b"
|
||||
|
||||
# Call the instance method on the class to expose the self argument
|
||||
x, y = MS_Class.instance_method(SOURCE, NONSOURCE)
|
||||
SINK(x) # $ MISSING: flow="SOURCE, l:-1 -> x"
|
||||
SINK_F(y)
|
||||
|
||||
# Call the instance method on the class to expose the self argument
|
||||
# That self argument is not referenced by `Argument[self:]`
|
||||
SINK_F(MS_Class.explicit_self(SOURCE))
|
||||
# Instead, `Argument[self:]` refers to a keyword argument named `self` (which you are allowed to do in Python)
|
||||
SINK(c.explicit_self(self = SOURCE)) # $ flow="SOURCE -> c.explicit_self(..)"
|
||||
|
||||
# Modeled flow-summary is not value preserving
|
||||
from json import MS_loads as json_loads
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import json
|
||||
|
||||
from flask import Flask, make_response, jsonify, Response, request, redirect
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
@@ -117,7 +118,7 @@ def response_modification1(): # $requestHandler
|
||||
@app.route("/content-type/response-modification2") # $routeSetup="/content-type/response-modification2"
|
||||
def response_modification2(): # $requestHandler
|
||||
resp = make_response("<h1>hello</h1>") # $HttpResponse mimetype=text/html responseBody="<h1>hello</h1>"
|
||||
resp.headers["content-type"] = "text/plain" # $ MISSING: HttpResponse mimetype=text/plain
|
||||
resp.headers["content-type"] = "text/plain" # $ headerWriteNameUnsanitized="content-type" headerWriteValueSanitized="text/plain" MISSING: HttpResponse mimetype=text/plain
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
|
||||
|
||||
|
||||
@@ -147,7 +148,7 @@ def Response3(): # $requestHandler
|
||||
@app.route("/content-type/Response4") # $routeSetup="/content-type/Response4"
|
||||
def Response4(): # $requestHandler
|
||||
# note: capitalization of Content-Type does not matter
|
||||
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/plain"}) # $HttpResponse responseBody="<h1>hello</h1>" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain
|
||||
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/plain"}) # $ headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized HttpResponse responseBody="<h1>hello</h1>" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
|
||||
|
||||
|
||||
@@ -155,7 +156,7 @@ def Response4(): # $requestHandler
|
||||
def Response5(): # $requestHandler
|
||||
# content_type argument takes priority (and result is text/plain)
|
||||
# note: capitalization of Content-Type does not matter
|
||||
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
|
||||
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, content_type="text/plain; charset=utf-8") # $ headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
|
||||
|
||||
|
||||
@@ -163,7 +164,7 @@ def Response5(): # $requestHandler
|
||||
def Response6(): # $requestHandler
|
||||
# mimetype argument takes priority over header (and result is text/plain)
|
||||
# note: capitalization of Content-Type does not matter
|
||||
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
|
||||
resp = Response("<h1>hello</h1>", headers={"Content-TYPE": "text/html"}, mimetype="text/plain") # $ headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized HttpResponse mimetype=text/plain responseBody="<h1>hello</h1>"
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
|
||||
|
||||
|
||||
@@ -207,12 +208,45 @@ def setting_cookie(): # $requestHandler
|
||||
resp = make_response() # $ HttpResponse mimetype=text/html
|
||||
resp.set_cookie("key", "value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
resp.set_cookie(key="key", value="value") # $ CookieWrite CookieName="key" CookieValue="value"
|
||||
resp.headers.add("Set-Cookie", "key2=value2") # $ MISSING: CookieWrite CookieRawHeader="key2=value2"
|
||||
resp.headers.add("Set-Cookie", "key2=value2") # $ headerWriteNameUnsanitized="Set-Cookie" headerWriteValueSanitized="key2=value2" MISSING: CookieWrite CookieRawHeader="key2=value2"
|
||||
resp.delete_cookie("key3") # $ CookieWrite CookieName="key3"
|
||||
resp.delete_cookie(key="key3") # $ CookieWrite CookieName="key3"
|
||||
return resp # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp
|
||||
|
||||
################################################################################
|
||||
# Headers
|
||||
################################################################################
|
||||
|
||||
@app.route("/headers") # $routeSetup="/headers"
|
||||
def headers(): # $requestHandler
|
||||
resp1 = Response() # $ HttpResponse mimetype=text/html
|
||||
resp1.headers["X-MyHeader"] = "a" # $ headerWriteNameUnsanitized="X-MyHeader" headerWriteValueSanitized="a"
|
||||
resp2 = make_response() # $ HttpResponse mimetype=text/html
|
||||
resp2.headers["X-MyHeader"] = "aa" # $ headerWriteNameUnsanitized="X-MyHeader" headerWriteValueSanitized="aa"
|
||||
resp2.headers.extend({"X-MyHeader2": "b"}) # $ headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized
|
||||
resp3 = make_response("hello", 200, {"X-MyHeader3": "c"}) # $ HttpResponse mimetype=text/html responseBody="hello" headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized
|
||||
resp4 = make_response("hello", {"X-MyHeader4": "d"}) # $ HttpResponse mimetype=text/html responseBody="hello" headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized
|
||||
resp5 = Response(headers={"X-MyHeader5":"e"}) # $ HttpResponse mimetype=text/html headerWriteBulk=Dict headerWriteNameUnsanitized headerWriteValueSanitized
|
||||
return resp5 # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=resp5
|
||||
|
||||
@app.route("/werkzeug-headers") # $routeSetup="/werkzeug-headers"
|
||||
def werkzeug_headers(): # $requestHandler
|
||||
response = Response() # $ HttpResponse mimetype=text/html
|
||||
headers = Headers()
|
||||
headers.add("X-MyHeader1", "a") # $ headerWriteNameUnsanitized="X-MyHeader1" headerWriteValueSanitized="a"
|
||||
headers.add_header("X-MyHeader2", "b") # $ headerWriteNameUnsanitized="X-MyHeader2" headerWriteValueSanitized="b"
|
||||
headers.set("X-MyHeader3", "c") # $ headerWriteNameUnsanitized="X-MyHeader3" headerWriteValueSanitized="c"
|
||||
headers.setdefault("X-MyHeader4", "d") # $ headerWriteNameUnsanitized="X-MyHeader4" headerWriteValueSanitized="d"
|
||||
headers.__setitem__("X-MyHeader5", "e") # $ headerWriteNameUnsanitized="X-MyHeader5" headerWriteValueSanitized="e"
|
||||
headers["X-MyHeader6"] = "f" # $ headerWriteNameUnsanitized="X-MyHeader6" headerWriteValueSanitized="f"
|
||||
h1 = {"X-MyHeader7": "g"}
|
||||
headers.extend(h1) # $ headerWriteBulk=h1 headerWriteNameUnsanitized headerWriteValueSanitized
|
||||
h2 = [("X-MyHeader8", "h")]
|
||||
headers.extend(h2) # $ headerWriteBulk=h2 headerWriteNameUnsanitized headerWriteValueSanitized
|
||||
response.headers = headers
|
||||
return response # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=response
|
||||
|
||||
################################################################################
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
@@ -0,0 +1,4 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
import MakeInlineTaintTest<TestTaintTrackingConfig>
|
||||
153
python/ql/test/library-tests/frameworks/pyramid/pyramid_test.py
Normal file
153
python/ql/test/library-tests/frameworks/pyramid/pyramid_test.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from pyramid.view import view_config
|
||||
from pyramid.config import Configurator
|
||||
from pyramid.response import Response
|
||||
from pyramid.httpexceptions import HTTPMultipleChoices, HTTPMovedPermanently, HTTPFound, HTTPSeeOther, HTTPUseProxy, HTTPTemporaryRedirect, HTTPPermanentRedirect
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
def ignore(*args, **kwargs): pass
|
||||
ensure_tainted = ensure_not_tainted = ignore
|
||||
|
||||
@view_config(route_name="test1") # $ routeSetup
|
||||
def test1(request): # $ requestHandler
|
||||
ensure_tainted(
|
||||
request, # $ tainted
|
||||
|
||||
request.accept, # $ tainted
|
||||
request.accept_charset, # $ tainted
|
||||
request.accept_encoding, # $ tainted
|
||||
request.accept_language, # $ tainted
|
||||
request.authorization, # $ tainted
|
||||
request.cache_control, # $ tainted
|
||||
request.client_addr, # $ tainted
|
||||
request.content_type, # $ tainted
|
||||
request.domain, # $ tainted
|
||||
request.host, # $ tainted
|
||||
request.host_port, # $ tainted
|
||||
request.host_url, # $ tainted
|
||||
request.if_match, # $ tainted
|
||||
request.if_none_match, # $ tainted
|
||||
request.if_range, # $ tainted
|
||||
request.pragma, # $ tainted
|
||||
request.range, # $ tainted
|
||||
request.referer, # $ tainted
|
||||
request.referrer, # $ tainted
|
||||
request.user_agent, # $ tainted
|
||||
|
||||
request.as_bytes, # $ tainted
|
||||
|
||||
request.body, # $ tainted
|
||||
request.body_file.read(), # $ tainted
|
||||
request.body_file_raw.read(), # $ tainted
|
||||
request.body_file_seekable.read(),# $ tainted
|
||||
|
||||
request.json, # $ tainted
|
||||
request.json_body, # $ tainted
|
||||
request.json['a']['b'][0]['c'], # $ tainted
|
||||
|
||||
request.text, # $ tainted
|
||||
|
||||
request.matchdict, # $ tainted
|
||||
|
||||
request.path, # $ tainted
|
||||
request.path_info, # $ tainted
|
||||
request.path_info_peek(), # $ tainted
|
||||
request.path_info_pop(), # $ tainted
|
||||
request.path_qs, # $ tainted
|
||||
request.path_url, # $ tainted
|
||||
request.query_string, # $ tainted
|
||||
|
||||
request.url, # $ tainted
|
||||
request.urlargs, # $ tainted
|
||||
request.urlvars, # $ tainted
|
||||
|
||||
request.GET['a'], # $ tainted
|
||||
request.POST['b'], # $ tainted
|
||||
request.cookies['c'], # $ tainted
|
||||
request.params['d'], # $ tainted
|
||||
request.headers['X-My-Header'], # $ tainted
|
||||
request.GET.values(), # $ tainted
|
||||
|
||||
request.copy(), # $ tainted
|
||||
request.copy_get(), # $ tainted
|
||||
request.copy().GET['a'], # $ tainted
|
||||
request.copy_get().body # $ tainted
|
||||
)
|
||||
|
||||
return Response("Ok") # $ HttpResponse responseBody="Ok" mimetype=text/html
|
||||
|
||||
def test2(request): # $ requestHandler
|
||||
ensure_tainted(request) # $ tainted
|
||||
|
||||
resp = Response("Ok", content_type="text/plain") # $ HttpResponse responseBody="Ok" mimetype=text/plain
|
||||
resp.body = "Ok2" # $ HttpResponse responseBody="Ok2" SPURIOUS: mimetype=text/html
|
||||
return resp
|
||||
|
||||
@view_config(route_name="test3", renderer="string") # $ routeSetup
|
||||
def test3(ctx, req): # $ requestHandler
|
||||
ensure_tainted(req) # $ tainted
|
||||
resp = req.response # $ HttpResponse mimetype=text/html
|
||||
resp.set_cookie("hi", "there") # $ CookieWrite CookieName="hi" CookieValue="there"
|
||||
resp.set_cookie(value="there", name="hi") # $ CookieWrite CookieName="hi" CookieValue="there"
|
||||
return "Ok" # $ HttpResponse responseBody="Ok" mimetype=text/html
|
||||
|
||||
@view_config(route_name="test4", renderer="string") # $ routeSetup
|
||||
def test4(request): # $ requestHandler
|
||||
a = HTTPMultipleChoices("redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
b = HTTPMovedPermanently(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
c = HTTPFound(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
d = HTTPSeeOther(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
e = HTTPUseProxy(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
f = HTTPTemporaryRedirect(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
g = HTTPPermanentRedirect(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
raise a
|
||||
|
||||
# Unsupported cases
|
||||
class Test5:
|
||||
def __init__(self, request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
self.req = request
|
||||
|
||||
@view_config(route_name="test5", renderer="string") # $ routeSetup
|
||||
def test5(self): # $ requestHandler
|
||||
ensure_not_tainted(self) # $ SPURIOUS: tainted
|
||||
ensure_tainted(self.req) # $ MISSING: tainted
|
||||
return "Ok" # $ HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
@view_config(route_name="test6", attr="test6method", renderer="string") # $ routeSetup
|
||||
class Test6:
|
||||
def __init__(self, request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
self.req = request
|
||||
|
||||
def test6method(self): # $ MISSING: requestHandler
|
||||
ensure_not_tainted(self)
|
||||
ensure_tainted(self.req) # $ MISSING: tainted
|
||||
return "Ok" # $ MISSING: HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
@view_config(route_name="test6", renderer="string") # $ routeSetup
|
||||
class Test6:
|
||||
def __init__(self, context, request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
self.req = request
|
||||
|
||||
def __call__(self): # $ MISSING: requestHandler
|
||||
ensure_not_tainted(self)
|
||||
ensure_tainted(self.req) # $ MISSING: tainted
|
||||
return "Ok" # $ MISSING: HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
class Test7:
|
||||
def __call__(self,context,request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
return "Ok" # $ MISSING: HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with Configurator() as config:
|
||||
for i in range(1,8):
|
||||
config.add_route(f"test{i}", f"/test{i}")
|
||||
config.add_view(test2, route_name="test2") # $ routeSetup
|
||||
config.add_view(Test7(), route_name="test7", renderer="string") # $ routeSetup
|
||||
config.scan()
|
||||
server = make_server('127.0.0.1', 8080, config.make_wsgi_app())
|
||||
print("serving")
|
||||
server.serve_forever()
|
||||
@@ -2,6 +2,7 @@
|
||||
# see https://docs.python.org/3/library/wsgiref.html#wsgiref.simple_server.WSGIServer
|
||||
import sys
|
||||
import wsgiref.simple_server
|
||||
import wsgiref.headers
|
||||
|
||||
def ignore(*arg, **kwargs): pass
|
||||
ensure_tainted = ensure_not_tainted = ignore
|
||||
@@ -17,7 +18,7 @@ def func(environ, start_response): # $ requestHandler
|
||||
environ, # $ tainted
|
||||
environ["PATH_INFO"], # $ tainted
|
||||
)
|
||||
write = start_response("200 OK", [("Content-Type", "text/plain")])
|
||||
write = start_response("200 OK", [("Content-Type", "text/plain")]) # $ headerWriteBulk=List headerWriteNameUnsanitized headerWriteValueUnsanitized
|
||||
write(b"hello") # $ HttpResponse responseBody=b"hello"
|
||||
write(data=b" ") # $ HttpResponse responseBody=b" "
|
||||
|
||||
@@ -32,9 +33,17 @@ class MyServer(wsgiref.simple_server.WSGIServer):
|
||||
self.set_app(self.my_method)
|
||||
|
||||
def my_method(self, _env, start_response): # $ requestHandler
|
||||
start_response("200 OK", [])
|
||||
start_response("200 OK", []) # $ headerWriteBulk=List headerWriteNameUnsanitized headerWriteValueUnsanitized
|
||||
return [b"my_method"] # $ HttpResponse responseBody=List
|
||||
|
||||
def func2(environ, start_response): # $ requestHandler
|
||||
headers = wsgiref.headers.Headers([("Content-Type", "text/plain")]) # $ headerWriteBulk=List headerWriteNameUnsanitized headerWriteValueUnsanitized
|
||||
headers.add_header("X-MyHeader", "a") # $ headerWriteNameUnsanitized="X-MyHeader" headerWriteValueUnsanitized="a"
|
||||
headers.setdefault("X-MyHeader2", "b") # $ headerWriteNameUnsanitized="X-MyHeader2" headerWriteValueUnsanitized="b"
|
||||
headers.__setitem__("X-MyHeader3", "c") # $ headerWriteNameUnsanitized="X-MyHeader3" headerWriteValueUnsanitized="c"
|
||||
headers["X-MyHeader4"] = "d" # $ headerWriteNameUnsanitized="X-MyHeader4" headerWriteValueUnsanitized="d"
|
||||
start_response(status, headers) # $ headerWriteBulk=headers headerWriteNameUnsanitized headerWriteValueUnsanitized
|
||||
return [b"Hello"] # $ HttpResponse responseBody=List
|
||||
|
||||
case = sys.argv[1]
|
||||
if case == "1":
|
||||
@@ -45,9 +54,11 @@ elif case == "2":
|
||||
elif case == "3":
|
||||
server = MyServer()
|
||||
def func3(_env, start_response): # $ requestHandler
|
||||
start_response("200 OK", [])
|
||||
start_response("200 OK", []) # $ headerWriteBulk=List headerWriteNameUnsanitized headerWriteValueUnsanitized
|
||||
return [b"foo"] # $ HttpResponse responseBody=List
|
||||
server.set_app(func3)
|
||||
elif case == "4":
|
||||
server = wsgiref.simple_server.make_server(ADDRESS[0], ADDRESS[1], func2)
|
||||
else:
|
||||
sys.exit("wrong case")
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
edges
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:1:29:1:35 | ControlFlowNode for request | provenance | |
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:9:18:9:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:19:18:19:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:20:36:20:42 | ControlFlowNode for request | provenance | |
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | flask_tests.py:31:18:31:24 | ControlFlowNode for request | provenance | |
|
||||
| flask_tests.py:9:5:9:14 | ControlFlowNode for rfs_header | flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_tests.py:9:18:9:24 | ControlFlowNode for request | flask_tests.py:9:5:9:14 | ControlFlowNode for rfs_header | provenance | AdditionalTaintStep |
|
||||
| flask_tests.py:19:18:19:24 | ControlFlowNode for request | flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | provenance | AdditionalTaintStep |
|
||||
| flask_tests.py:20:36:20:42 | ControlFlowNode for request | flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | provenance | AdditionalTaintStep |
|
||||
| flask_tests.py:31:5:31:14 | ControlFlowNode for rfs_header | flask_tests.py:33:11:33:20 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_tests.py:31:5:31:14 | ControlFlowNode for rfs_header | flask_tests.py:35:12:35:21 | ControlFlowNode for rfs_header | provenance | |
|
||||
| flask_tests.py:31:18:31:24 | ControlFlowNode for request | flask_tests.py:31:5:31:14 | ControlFlowNode for rfs_header | provenance | AdditionalTaintStep |
|
||||
| wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | wsgiref_tests.py:6:5:6:10 | ControlFlowNode for h_name | provenance | |
|
||||
| wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | wsgiref_tests.py:7:5:7:9 | ControlFlowNode for h_val | provenance | |
|
||||
| wsgiref_tests.py:6:5:6:10 | ControlFlowNode for h_name | wsgiref_tests.py:8:17:8:22 | ControlFlowNode for h_name | provenance | |
|
||||
| wsgiref_tests.py:7:5:7:9 | ControlFlowNode for h_val | wsgiref_tests.py:8:42:8:46 | ControlFlowNode for h_val | provenance | |
|
||||
nodes
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | semmle.label | ControlFlowNode for ImportMember |
|
||||
| flask_tests.py:1:29:1:35 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_tests.py:9:5:9:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_tests.py:9:18:9:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_tests.py:19:18:19:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_tests.py:20:36:20:42 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
|
||||
| flask_tests.py:31:5:31:14 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_tests.py:31:18:31:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
|
||||
| flask_tests.py:33:11:33:20 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| flask_tests.py:35:12:35:21 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
|
||||
| wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | semmle.label | ControlFlowNode for environ |
|
||||
| wsgiref_tests.py:6:5:6:10 | ControlFlowNode for h_name | semmle.label | ControlFlowNode for h_name |
|
||||
| wsgiref_tests.py:7:5:7:9 | ControlFlowNode for h_val | semmle.label | ControlFlowNode for h_val |
|
||||
| wsgiref_tests.py:8:17:8:22 | ControlFlowNode for h_name | semmle.label | ControlFlowNode for h_name |
|
||||
| wsgiref_tests.py:8:42:8:46 | ControlFlowNode for h_val | semmle.label | ControlFlowNode for h_val |
|
||||
subpaths
|
||||
#select
|
||||
| flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:13:17:13:26 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:20:36:20:61 | ControlFlowNode for Subscript | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| flask_tests.py:33:11:33:20 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:33:11:33:20 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| flask_tests.py:35:12:35:21 | ControlFlowNode for rfs_header | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | flask_tests.py:35:12:35:21 | ControlFlowNode for rfs_header | This HTTP header is constructed from a $@. | flask_tests.py:1:29:1:35 | ControlFlowNode for ImportMember | user-provided value |
|
||||
| wsgiref_tests.py:8:17:8:22 | ControlFlowNode for h_name | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | wsgiref_tests.py:8:17:8:22 | ControlFlowNode for h_name | This HTTP header is constructed from a $@. | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | user-provided value |
|
||||
| wsgiref_tests.py:8:42:8:46 | ControlFlowNode for h_val | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | wsgiref_tests.py:8:42:8:46 | ControlFlowNode for h_val | This HTTP header is constructed from a $@. | wsgiref_tests.py:4:14:4:20 | ControlFlowNode for environ | user-provided value |
|
||||
@@ -0,0 +1 @@
|
||||
Security/CWE-113/HeaderInjection.ql
|
||||
@@ -0,0 +1,40 @@
|
||||
from flask import Response, request, Flask, make_response
|
||||
from werkzeug.datastructures import Headers
|
||||
|
||||
app = Flask(__name__)
|
||||
|
||||
|
||||
@app.route('/werkzeug_headers')
|
||||
def werkzeug_headers():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
headers = Headers()
|
||||
headers.add("HeaderName", rfs_header) # GOOD: Newlines are rejected from header value.
|
||||
headers.add(rfs_header, "HeaderValue") # BAD: User controls header name.
|
||||
response.headers = headers
|
||||
return response
|
||||
|
||||
@app.route("/flask_make_response_header_arg2")
|
||||
def flask_make_response_header_arg2():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
resp = make_response("hello", {request.args["rfs_header"]: "HeaderValue"}) # BAD
|
||||
return resp
|
||||
|
||||
@app.route("/flask_escaped")
|
||||
def flask_escaped():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
resp = make_response("hello", {rfs_header.replace("\n", ""): "HeaderValue"}) # GOOD - Newlines are removed from the input.
|
||||
return resp
|
||||
|
||||
@app.route("/flask_extend")
|
||||
def flask_extend():
|
||||
rfs_header = request.args["rfs_header"]
|
||||
response = Response()
|
||||
h1 = {rfs_header: "HeaderValue"}
|
||||
response.headers.extend(h1) # BAD
|
||||
h2 = [(rfs_header, "HeaderValue")]
|
||||
response.headers.extend(h2) # BAD
|
||||
return response
|
||||
|
||||
# if __name__ == "__main__":
|
||||
# app.run(debug=True)
|
||||
@@ -0,0 +1,15 @@
|
||||
from wsgiref.simple_server import make_server
|
||||
from wsgiref.headers import Headers
|
||||
|
||||
def test_app(environ, start_response):
|
||||
status = "200 OK"
|
||||
h_name = environ["source_n"]
|
||||
h_val = environ["source_v"]
|
||||
headers = [(h_name, "val"), ("name", h_val)]
|
||||
start_response(status, headers) # BAD
|
||||
return [b"Hello"]
|
||||
|
||||
def main1():
|
||||
with make_server('', 8000, test_app) as httpd:
|
||||
print("Serving on port 8000...")
|
||||
httpd.serve_forever()
|
||||
@@ -0,0 +1,4 @@
|
||||
edges
|
||||
nodes
|
||||
subpaths
|
||||
#select
|
||||
@@ -0,0 +1 @@
|
||||
Security/CWE-113/HeaderInjection.ql
|
||||
@@ -0,0 +1,8 @@
|
||||
source
|
||||
| wsgiref_tests.py:7:14:7:20 | ControlFlowNode for environ |
|
||||
sink
|
||||
headerWrite
|
||||
| wsgiref_tests.py:12:5:12:35 | ControlFlowNode for start_response() | wsgiref_tests.py:11:17:11:22 | ControlFlowNode for h_name | wsgiref_tests.py:11:25:11:29 | ControlFlowNode for StringLiteral | false | false |
|
||||
| wsgiref_tests.py:12:5:12:35 | ControlFlowNode for start_response() | wsgiref_tests.py:11:17:11:22 | ControlFlowNode for h_name | wsgiref_tests.py:11:42:11:46 | ControlFlowNode for h_val | false | false |
|
||||
| wsgiref_tests.py:12:5:12:35 | ControlFlowNode for start_response() | wsgiref_tests.py:11:34:11:39 | ControlFlowNode for StringLiteral | wsgiref_tests.py:11:25:11:29 | ControlFlowNode for StringLiteral | false | false |
|
||||
| wsgiref_tests.py:12:5:12:35 | ControlFlowNode for start_response() | wsgiref_tests.py:11:34:11:39 | ControlFlowNode for StringLiteral | wsgiref_tests.py:11:42:11:46 | ControlFlowNode for h_val | false | false |
|
||||
@@ -0,0 +1,20 @@
|
||||
import python
|
||||
import semmle.python.security.dataflow.HttpHeaderInjectionCustomizations
|
||||
import semmle.python.dataflow.new.DataFlow
|
||||
import semmle.python.Concepts
|
||||
|
||||
query predicate source(HttpHeaderInjection::Source src) {
|
||||
src.getLocation().getFile().getBaseName() in ["wsgiref_tests.py", "flask_tests.py"]
|
||||
}
|
||||
|
||||
query predicate sink(HttpHeaderInjection::Sink sink) { any() }
|
||||
|
||||
query predicate headerWrite(
|
||||
Http::Server::ResponseHeaderWrite write, DataFlow::Node name, DataFlow::Node val,
|
||||
boolean nameVuln, boolean valVuln
|
||||
) {
|
||||
name = write.getNameArg() and
|
||||
val = write.getValueArg() and
|
||||
(if write.nameAllowsNewline() then nameVuln = true else nameVuln = false) and
|
||||
(if write.valueAllowsNewline() then valVuln = true else valVuln = false)
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
# These tests use a wsgi validator; so are split into a separate directory from the other tests since the models only check for the presence of a validator in the database.
|
||||
|
||||
from wsgiref.simple_server import make_server
|
||||
from wsgiref.headers import Headers
|
||||
from wsgiref.validate import validator
|
||||
|
||||
def test_app(environ, start_response):
|
||||
status = "200 OK"
|
||||
h_name = environ["source_n"]
|
||||
h_val = environ["source_v"]
|
||||
headers = [(h_name, "val"), ("name", h_val)]
|
||||
start_response(status, headers) # GOOD - the application is validated, so headers containing newlines will be rejected.
|
||||
return [b"Hello"]
|
||||
|
||||
def main1():
|
||||
with make_server('', 8000, validator(test_app)) as httpd:
|
||||
print("Serving on port 8000...")
|
||||
httpd.serve_forever()
|
||||
Reference in New Issue
Block a user