Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Python: Promote Header Injection query from experimental #16105

Merged
Merged
Show file tree
Hide file tree
Changes from 22 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
6021d92
Move headers injection query and concept from experimental to main
joefarebrother Mar 27, 2024
25ffcb2
Split into customizations file
joefarebrother Mar 27, 2024
68d9091
Add to header write concept a specification of whether the name or va…
joefarebrother Apr 2, 2024
b9984be
Add test cases
joefarebrother Apr 3, 2024
3e9341f
Model class instantiation for werkzueg headers
joefarebrother Apr 3, 2024
a88ad62
Implemented sinks for bulk header updates, and added corresponding te…
joefarebrother Apr 3, 2024
dbbc944
Correct spelling
joefarebrother Apr 3, 2024
fa28d94
Added a sanitizer for replacing newlines.
joefarebrother Apr 3, 2024
8636a50
Fix qldoc + remove deprecation from experimental concepts (as they ar…
joefarebrother Apr 3, 2024
daa31b5
Add documentation
joefarebrother Apr 5, 2024
cf8db4e
Update instances of experimental concept to the main one, and anotate…
joefarebrother Apr 5, 2024
9d56f3e
Fix qldoc formatting
joefarebrother Apr 5, 2024
eeef062
Implement sinks for wsgiref + allow lists in bulk header updates + lo…
joefarebrother Apr 8, 2024
d4a0728
Add more tests
joefarebrother Apr 8, 2024
f57ba3e
Add change note
joefarebrother Apr 8, 2024
f3b27d6
Add test case for validated wsgiref servers + fix typo
joefarebrother Apr 8, 2024
49e5f8a
Add tests for instances of the header write concept
joefarebrother Apr 8, 2024
1dce2eb
Rename to response splitting
joefarebrother Apr 23, 2024
ec4c820
Fix deprecation
joefarebrother Apr 23, 2024
2b935e5
Add concept tests + fix typo
joefarebrother Apr 24, 2024
8fb2faa
Add additional info to concept tests
joefarebrother Apr 24, 2024
53f69d9
Reduce query tests with cases covered by concept tests
joefarebrother Apr 24, 2024
0f7325e
Rename test directory and add comment to clarity on the purpose of sp…
joefarebrother May 7, 2024
efda63d
Update test output
joefarebrother May 7, 2024
ab23d0a
Merge branch 'main' into python-promote-header-injection
joefarebrother May 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
108 changes: 108 additions & 0 deletions python/ql/lib/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
37 changes: 37 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Flask.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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() }
}
}

// ---------------------------------------------------------------------------
Expand Down
133 changes: 131 additions & 2 deletions python/ql/lib/semmle/python/frameworks/Stdlib.qll
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand All @@ -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 }
}

/**
Expand Down Expand Up @@ -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() }
}
}
}

// ---------------------------------------------------------------------------
Expand Down
Loading
Loading