Implement sinks for wsgiref + allow lists in bulk header updates + local flow

This commit is contained in:
Joe Farebrother
2024-04-08 17:46:44 +01:00
parent 9d56f3eb68
commit eeef062f7c
3 changed files with 142 additions and 2 deletions

View File

@@ -1085,7 +1085,7 @@ module Http {
}
/**
* A data-flow node that sets multiple headers in an HTTP response using a dict.
* 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.

View File

@@ -2194,6 +2194,14 @@ module StdlibPrivate {
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app")
) 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")]
|
appArg = poorMansFunctionTracker(this)
)
@@ -2305,6 +2313,109 @@ 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("wsqiref.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) }
/** 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: implement validator
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { any() }
}
/**
* 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: implement validator
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { any() }
}
/** 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: implement validator
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { any() }
}
/** 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: implement validator
override predicate nameAllowsNewline() { any() }
override predicate valueAllowsNewline() { any() }
}
}
}
// ---------------------------------------------------------------------------

View File

@@ -57,7 +57,11 @@ module HttpHeaderInjection {
{
KeyValuePair item;
HeaderBulkWriteDictLiteral() { item = super.geBulkArg().asExpr().(Dict).getAnItem() }
HeaderBulkWriteDictLiteral() {
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.geBulkArg()) |
item = dict.getAnItem()
)
}
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
@@ -72,6 +76,31 @@ module HttpHeaderInjection {
}
}
/** 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.geBulkArg()) |
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.
*/