Skip to content

Commit eeef062

Browse files
Implement sinks for wsgiref + allow lists in bulk header updates + local flow
1 parent 9d56f3e commit eeef062

File tree

3 files changed

+142
-2
lines changed

3 files changed

+142
-2
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1085,7 +1085,7 @@ module Http {
10851085
}
10861086

10871087
/**
1088-
* A data-flow node that sets multiple headers in an HTTP response using a dict.
1088+
* A data-flow node that sets multiple headers in an HTTP response using a dict or a list of tuples.
10891089
*
10901090
* Extend this class to model new APIs. If you want to refine existing API models,
10911091
* extend `ResponseHeaderBulkWrite::Range` instead.

python/ql/lib/semmle/python/frameworks/Stdlib.qll

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2194,6 +2194,14 @@ module StdlibPrivate {
21942194
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app")
21952195
) and
21962196
appArg in [setAppCall.getArg(0), setAppCall.getArgByName("application")]
2197+
or
2198+
// `make_server` calls `set_app`
2199+
setAppCall =
2200+
API::moduleImport("wsgiref")
2201+
.getMember("simple_server")
2202+
.getMember("make_server")
2203+
.getACall() and
2204+
appArg in [setAppCall.getArg(2), setAppCall.getArgByName("app")]
21972205
|
21982206
appArg = poorMansFunctionTracker(this)
21992207
)
@@ -2305,6 +2313,109 @@ module StdlibPrivate {
23052313

23062314
override string getMimetypeDefault() { none() }
23072315
}
2316+
2317+
/**
2318+
* Provides models for the `wsgiref.headers.Headers` class
2319+
*
2320+
* See https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers.
2321+
*/
2322+
module Headers {
2323+
/** Gets a reference to the `wsgiref.headers.Headers` class. */
2324+
API::Node classRef() {
2325+
result = API::moduleImport("wsgiref").getMember("headers").getMember("Headers")
2326+
or
2327+
result = ModelOutput::getATypeNode("wsqiref.headers.Headers~Subclass").getASubclass*()
2328+
}
2329+
2330+
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
2331+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
2332+
t.start() and
2333+
result = classRef().getACall()
2334+
or
2335+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
2336+
}
2337+
2338+
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
2339+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
2340+
2341+
/** A class instantiation of `wsgiref.headers.Headers`, conidered as a write to a response header. */
2342+
private class WsgirefHeadersInstantiation extends Http::Server::ResponseHeaderBulkWrite::Range,
2343+
DataFlow::CallCfgNode
2344+
{
2345+
WsgirefHeadersInstantiation() { this = classRef().getACall() }
2346+
2347+
override DataFlow::Node getBulkArg() {
2348+
result = [this.getArg(0), this.getArgByName("headers")]
2349+
}
2350+
2351+
// TODO: implement validator
2352+
override predicate nameAllowsNewline() { any() }
2353+
2354+
override predicate valueAllowsNewline() { any() }
2355+
}
2356+
2357+
/**
2358+
* A call to a `start_response` function that sets the response headers.
2359+
*/
2360+
private class WsgirefSimpleServerSetHeaders extends Http::Server::ResponseHeaderBulkWrite::Range,
2361+
DataFlow::CallCfgNode
2362+
{
2363+
WsgirefSimpleServerSetHeaders() { this.getFunction() = startResponse() }
2364+
2365+
override DataFlow::Node getBulkArg() {
2366+
result = [this.getArg(1), this.getArgByName("headers")]
2367+
}
2368+
2369+
// TODO: implement validator
2370+
override predicate nameAllowsNewline() { any() }
2371+
2372+
override predicate valueAllowsNewline() { any() }
2373+
}
2374+
2375+
/** A call to a method that writes to a response header. */
2376+
private class HeaderWriteCall extends Http::Server::ResponseHeaderWrite::Range,
2377+
DataFlow::MethodCallNode
2378+
{
2379+
HeaderWriteCall() {
2380+
this.calls(instance(), ["add_header", "set", "setdefault", "__setitem__"])
2381+
}
2382+
2383+
override DataFlow::Node getNameArg() { result = this.getArg(0) }
2384+
2385+
override DataFlow::Node getValueArg() { result = this.getArg(1) }
2386+
2387+
// TODO: implement validator
2388+
override predicate nameAllowsNewline() { any() }
2389+
2390+
override predicate valueAllowsNewline() { any() }
2391+
}
2392+
2393+
/** A dict-like write to a response header. */
2394+
private class HeaderWriteSubscript extends Http::Server::ResponseHeaderWrite::Range,
2395+
DataFlow::Node
2396+
{
2397+
DataFlow::Node name;
2398+
DataFlow::Node value;
2399+
2400+
HeaderWriteSubscript() {
2401+
exists(SubscriptNode subscript |
2402+
this.asCfgNode() = subscript and
2403+
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
2404+
name.asCfgNode() = subscript.getIndex() and
2405+
subscript.getObject() = instance().asCfgNode()
2406+
)
2407+
}
2408+
2409+
override DataFlow::Node getNameArg() { result = name }
2410+
2411+
override DataFlow::Node getValueArg() { result = value }
2412+
2413+
// TODO: implement validator
2414+
override predicate nameAllowsNewline() { any() }
2415+
2416+
override predicate valueAllowsNewline() { any() }
2417+
}
2418+
}
23082419
}
23092420

23102421
// ---------------------------------------------------------------------------

python/ql/lib/semmle/python/security/dataflow/HttpHeaderInjectionCustomizations.qll

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,11 @@ module HttpHeaderInjection {
5757
{
5858
KeyValuePair item;
5959

60-
HeaderBulkWriteDictLiteral() { item = super.geBulkArg().asExpr().(Dict).getAnItem() }
60+
HeaderBulkWriteDictLiteral() {
61+
exists(Dict dict | DataFlow::localFlow(DataFlow::exprNode(dict), super.geBulkArg()) |
62+
item = dict.getAnItem()
63+
)
64+
}
6165

6266
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
6367

@@ -72,6 +76,31 @@ module HttpHeaderInjection {
7276
}
7377
}
7478

79+
/** A tuple in a list for a bulk header update, considered as a single header update. */
80+
// TODO: We could instead consider bulk writes as sinks with implicit read steps as needed.
81+
private class HeaderBulkWriteListLiteral extends Http::Server::ResponseHeaderWrite::Range instanceof Http::Server::ResponseHeaderBulkWrite
82+
{
83+
Tuple item;
84+
85+
HeaderBulkWriteListLiteral() {
86+
exists(List list | DataFlow::localFlow(DataFlow::exprNode(list), super.geBulkArg()) |
87+
item = list.getAnElt()
88+
)
89+
}
90+
91+
override DataFlow::Node getNameArg() { result.asExpr() = item.getElt(0) }
92+
93+
override DataFlow::Node getValueArg() { result.asExpr() = item.getElt(1) }
94+
95+
override predicate nameAllowsNewline() {
96+
Http::Server::ResponseHeaderBulkWrite.super.nameAllowsNewline()
97+
}
98+
99+
override predicate valueAllowsNewline() {
100+
Http::Server::ResponseHeaderBulkWrite.super.valueAllowsNewline()
101+
}
102+
}
103+
75104
/**
76105
* A call to replace line breaks, considered as a sanitizer.
77106
*/

0 commit comments

Comments
 (0)