Skip to content

Commit 904799b

Browse files
Merge pull request #16105 from joefarebrother/python-promote-header-injection
Python: Promote Header Injection query from experimental
2 parents 79c6834 + ab23d0a commit 904799b

38 files changed

+827
-378
lines changed

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

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1025,6 +1025,114 @@ module Http {
10251025
}
10261026
}
10271027

1028+
/**
1029+
* A data-flow node that sets a header in an HTTP response.
1030+
*
1031+
* Extend this class to model new APIs. If you want to refine existing API models,
1032+
* extend `ResponseHeaderWrite::Range` instead.
1033+
*/
1034+
class ResponseHeaderWrite extends DataFlow::Node instanceof ResponseHeaderWrite::Range {
1035+
/**
1036+
* Gets the argument containing the header name.
1037+
*/
1038+
DataFlow::Node getNameArg() { result = super.getNameArg() }
1039+
1040+
/**
1041+
* Gets the argument containing the header value.
1042+
*/
1043+
DataFlow::Node getValueArg() { result = super.getValueArg() }
1044+
1045+
/**
1046+
* Holds if newlines are accepted in the header name argument.
1047+
*/
1048+
predicate nameAllowsNewline() { super.nameAllowsNewline() }
1049+
1050+
/**
1051+
* Holds if newlines are accepted in the header value argument.
1052+
*/
1053+
predicate valueAllowsNewline() { super.valueAllowsNewline() }
1054+
}
1055+
1056+
/** Provides a class for modeling header writes on HTTP responses. */
1057+
module ResponseHeaderWrite {
1058+
/**
1059+
*A data-flow node that sets a header in an HTTP response.
1060+
*
1061+
* Extend this class to model new APIs. If you want to refine existing API models,
1062+
* extend `ResponseHeaderWrite` instead.
1063+
*/
1064+
abstract class Range extends DataFlow::Node {
1065+
/**
1066+
* Gets the argument containing the header name.
1067+
*/
1068+
abstract DataFlow::Node getNameArg();
1069+
1070+
/**
1071+
* Gets the argument containing the header value.
1072+
*/
1073+
abstract DataFlow::Node getValueArg();
1074+
1075+
/**
1076+
* Holds if newlines are accepted in the header name argument.
1077+
*/
1078+
abstract predicate nameAllowsNewline();
1079+
1080+
/**
1081+
* Holds if newlines are accepted in the header value argument.
1082+
*/
1083+
abstract predicate valueAllowsNewline();
1084+
}
1085+
}
1086+
1087+
/**
1088+
* A data-flow node that sets multiple headers in an HTTP response using a dict or a list of tuples.
1089+
*
1090+
* Extend this class to model new APIs. If you want to refine existing API models,
1091+
* extend `ResponseHeaderBulkWrite::Range` instead.
1092+
*/
1093+
class ResponseHeaderBulkWrite extends DataFlow::Node instanceof ResponseHeaderBulkWrite::Range {
1094+
/**
1095+
* Gets the argument containing the headers dictionary.
1096+
*/
1097+
DataFlow::Node getBulkArg() { result = super.getBulkArg() }
1098+
1099+
/**
1100+
* Holds if newlines are accepted in the header name argument.
1101+
*/
1102+
predicate nameAllowsNewline() { super.nameAllowsNewline() }
1103+
1104+
/**
1105+
* Holds if newlines are accepted in the header value argument.
1106+
*/
1107+
predicate valueAllowsNewline() { super.valueAllowsNewline() }
1108+
}
1109+
1110+
/** Provides a class for modeling bulk header writes on HTTP responses. */
1111+
module ResponseHeaderBulkWrite {
1112+
/**
1113+
* A data-flow node that sets multiple headers in an HTTP response using a dict.
1114+
*
1115+
* Extend this class to model new APIs. If you want to refine existing API models,
1116+
* extend `ResponseHeaderBulkWrite` instead.
1117+
*/
1118+
abstract class Range extends DataFlow::Node {
1119+
/**
1120+
* Gets the argument containing the headers dictionary.
1121+
*/
1122+
abstract DataFlow::Node getBulkArg();
1123+
1124+
/**
1125+
* Holds if newlines are accepted in the header name argument.
1126+
*/
1127+
abstract predicate nameAllowsNewline();
1128+
1129+
/**
1130+
* Holds if newlines are accepted in the header value argument.
1131+
*/
1132+
abstract predicate valueAllowsNewline();
1133+
}
1134+
}
1135+
10281136
/**
10291137
* A data-flow node that sets a cookie in an HTTP response.
10301138
*

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

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,43 @@ module Flask {
220220

221221
/** Gets a reference to an instance of `flask.Response`. */
222222
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
223+
224+
/** An `Headers` instance that is part of a Flask response. */
225+
private class FlaskResponseHeadersInstances extends Werkzeug::Headers::InstanceSource {
226+
FlaskResponseHeadersInstances() {
227+
this.(DataFlow::AttrRead).getObject() = instance() and
228+
this.(DataFlow::AttrRead).getAttributeName() = "headers"
229+
}
230+
}
231+
232+
/** A class instantiation of `Response` that sets response headers. */
233+
private class ResponseClassHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range,
234+
ClassInstantiation
235+
{
236+
override DataFlow::Node getBulkArg() {
237+
result = [this.getArg(2), this.getArgByName("headers")]
238+
}
239+
240+
override predicate nameAllowsNewline() { any() }
241+
242+
override predicate valueAllowsNewline() { none() }
243+
}
244+
245+
/** A call to `make_response that sets response headers. */
246+
private class MakeResponseHeadersWrite extends Http::Server::ResponseHeaderBulkWrite::Range,
247+
FlaskMakeResponseCall
248+
{
249+
override DataFlow::Node getBulkArg() {
250+
result = this.getArg(2)
251+
or
252+
strictcount(this.getArg(_)) = 2 and
253+
result = this.getArg(1)
254+
}
255+
256+
override predicate nameAllowsNewline() { any() }
257+
258+
override predicate valueAllowsNewline() { none() }
259+
}
223260
}
224261

225262
// ---------------------------------------------------------------------------

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

Lines changed: 131 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2183,17 +2183,35 @@ module StdlibPrivate {
21832183
* for how a request is processed and given to an application.
21842184
*/
21852185
class WsgirefSimpleServerApplication extends Http::Server::RequestHandler::Range {
2186+
boolean validator;
2187+
21862188
WsgirefSimpleServerApplication() {
21872189
exists(DataFlow::Node appArg, DataFlow::CallCfgNode setAppCall |
21882190
(
21892191
setAppCall =
2190-
WsgirefSimpleServer::subclassRef().getReturn().getMember("set_app").getACall()
2192+
WsgirefSimpleServer::subclassRef().getReturn().getMember("set_app").getACall() and
2193+
validator = false
21912194
or
21922195
setAppCall
21932196
.(DataFlow::MethodCallNode)
2194-
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app")
2197+
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app") and
2198+
validator = false
2199+
or
2200+
// assume an application that is passed to `wsgiref.validate.validator` is eventually passed to `set_app`
2201+
setAppCall =
2202+
API::moduleImport("wsgiref").getMember("validate").getMember("validator").getACall() and
2203+
validator = true
21952204
) and
21962205
appArg in [setAppCall.getArg(0), setAppCall.getArgByName("application")]
2206+
or
2207+
// `make_server` calls `set_app`
2208+
setAppCall =
2209+
API::moduleImport("wsgiref")
2210+
.getMember("simple_server")
2211+
.getMember("make_server")
2212+
.getACall() and
2213+
appArg in [setAppCall.getArg(2), setAppCall.getArgByName("app")] and
2214+
validator = false
21972215
|
21982216
appArg = poorMansFunctionTracker(this)
21992217
)
@@ -2202,6 +2220,9 @@ module StdlibPrivate {
22022220
override Parameter getARoutedParameter() { none() }
22032221

22042222
override string getFramework() { result = "Stdlib: wsgiref.simple_server application" }
2223+
2224+
/** Holds if this simple server application was passed to `wsgiref.validate.validator`. */
2225+
predicate isValidated() { validator = true }
22052226
}
22062227

22072228
/**
@@ -2305,6 +2326,114 @@ module StdlibPrivate {
23052326

23062327
override string getMimetypeDefault() { none() }
23072328
}
2329+
2330+
/**
2331+
* Provides models for the `wsgiref.headers.Headers` class
2332+
*
2333+
* See https://docs.python.org/3/library/wsgiref.html#module-wsgiref.headers.
2334+
*/
2335+
module Headers {
2336+
/** Gets a reference to the `wsgiref.headers.Headers` class. */
2337+
API::Node classRef() {
2338+
result = API::moduleImport("wsgiref").getMember("headers").getMember("Headers")
2339+
or
2340+
result = ModelOutput::getATypeNode("wsgiref.headers.Headers~Subclass").getASubclass*()
2341+
}
2342+
2343+
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
2344+
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
2345+
t.start() and
2346+
result = classRef().getACall()
2347+
or
2348+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
2349+
}
2350+
2351+
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
2352+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
2353+
2354+
/** Holds if there exists an application that is validated by `wsgiref.validate.validator`. */
2355+
private predicate existsValidatedApplication() {
2356+
exists(WsgirefSimpleServerApplication app | app.isValidated())
2357+
}
2358+
2359+
/** A class instantiation of `wsgiref.headers.Headers`, conidered as a write to a response header. */
2360+
private class WsgirefHeadersInstantiation extends Http::Server::ResponseHeaderBulkWrite::Range,
2361+
DataFlow::CallCfgNode
2362+
{
2363+
WsgirefHeadersInstantiation() { this = classRef().getACall() }
2364+
2365+
override DataFlow::Node getBulkArg() {
2366+
result = [this.getArg(0), this.getArgByName("headers")]
2367+
}
2368+
2369+
// TODO: These checks perhaps could be made more precise.
2370+
override predicate nameAllowsNewline() { not existsValidatedApplication() }
2371+
2372+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
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: These checks perhaps could be made more precise.
2388+
override predicate nameAllowsNewline() { not existsValidatedApplication() }
2389+
2390+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
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: These checks perhaps could be made more precise.
2414+
override predicate nameAllowsNewline() { not existsValidatedApplication() }
2415+
2416+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
2417+
}
2418+
2419+
/**
2420+
* A call to a `start_response` function that sets the response headers.
2421+
*/
2422+
private class WsgirefSimpleServerSetHeaders extends Http::Server::ResponseHeaderBulkWrite::Range,
2423+
DataFlow::CallCfgNode
2424+
{
2425+
WsgirefSimpleServerSetHeaders() { this.getFunction() = startResponse() }
2426+
2427+
override DataFlow::Node getBulkArg() {
2428+
result = [this.getArg(1), this.getArgByName("headers")]
2429+
}
2430+
2431+
// TODO: These checks perhaps could be made more precise.
2432+
override predicate nameAllowsNewline() { not existsValidatedApplication() }
2433+
2434+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
2435+
}
2436+
}
23082437
}
23092438

23102439
// ---------------------------------------------------------------------------

0 commit comments

Comments
 (0)