mirror of
https://github.com/github/codeql.git
synced 2026-04-28 10:15:14 +02:00
Merge pull request #16105 from joefarebrother/python-promote-header-injection
Python: Promote Header Injection query from experimental
This commit is contained in:
@@ -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.
|
||||
*
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
@@ -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() }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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>;
|
||||
Reference in New Issue
Block a user