Skip to content

Commit f3b27d6

Browse files
Add test case for validated wsgiref servers + fix typo
1 parent f57ba3e commit f3b27d6

File tree

8 files changed

+87
-31
lines changed

8 files changed

+87
-31
lines changed

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

+49-31
Original file line numberDiff line numberDiff line change
@@ -2183,15 +2183,24 @@ 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")]
21972206
or
@@ -2201,7 +2210,8 @@ module StdlibPrivate {
22012210
.getMember("simple_server")
22022211
.getMember("make_server")
22032212
.getACall() and
2204-
appArg in [setAppCall.getArg(2), setAppCall.getArgByName("app")]
2213+
appArg in [setAppCall.getArg(2), setAppCall.getArgByName("app")] and
2214+
validator = false
22052215
|
22062216
appArg = poorMansFunctionTracker(this)
22072217
)
@@ -2210,6 +2220,9 @@ module StdlibPrivate {
22102220
override Parameter getARoutedParameter() { none() }
22112221

22122222
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 }
22132226
}
22142227

22152228
/**
@@ -2324,7 +2337,7 @@ module StdlibPrivate {
23242337
API::Node classRef() {
23252338
result = API::moduleImport("wsgiref").getMember("headers").getMember("Headers")
23262339
or
2327-
result = ModelOutput::getATypeNode("wsqiref.headers.Headers~Subclass").getASubclass*()
2340+
result = ModelOutput::getATypeNode("wsgiref.headers.Headers~Subclass").getASubclass*()
23282341
}
23292342

23302343
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
@@ -2338,6 +2351,11 @@ module StdlibPrivate {
23382351
/** Gets a reference to an instance of `wsgiref.headers.Headers`. */
23392352
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
23402353

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+
23412359
/** A class instantiation of `wsgiref.headers.Headers`, conidered as a write to a response header. */
23422360
private class WsgirefHeadersInstantiation extends Http::Server::ResponseHeaderBulkWrite::Range,
23432361
DataFlow::CallCfgNode
@@ -2348,28 +2366,10 @@ module StdlibPrivate {
23482366
result = [this.getArg(0), this.getArgByName("headers")]
23492367
}
23502368

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() }
2369+
// TODO: These checks perhaps could be made more precise.
2370+
override predicate nameAllowsNewline() { not existsValidatedApplication() }
23712371

2372-
override predicate valueAllowsNewline() { any() }
2372+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
23732373
}
23742374

23752375
/** A call to a method that writes to a response header. */
@@ -2384,10 +2384,10 @@ module StdlibPrivate {
23842384

23852385
override DataFlow::Node getValueArg() { result = this.getArg(1) }
23862386

2387-
// TODO: implement validator
2388-
override predicate nameAllowsNewline() { any() }
2387+
// TODO: These checks perhaps could be made more precise.
2388+
override predicate nameAllowsNewline() { not existsValidatedApplication() }
23892389

2390-
override predicate valueAllowsNewline() { any() }
2390+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
23912391
}
23922392

23932393
/** A dict-like write to a response header. */
@@ -2410,10 +2410,28 @@ module StdlibPrivate {
24102410

24112411
override DataFlow::Node getValueArg() { result = value }
24122412

2413-
// TODO: implement validator
2414-
override predicate nameAllowsNewline() { any() }
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() }
24152433

2416-
override predicate valueAllowsNewline() { any() }
2434+
override predicate valueAllowsNewline() { not existsValidatedApplication() }
24172435
}
24182436
}
24192437
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
edges
2+
nodes
3+
subpaths
4+
#select
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Security/CWE-113/HeaderInjection.ql
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
from wsgiref.simple_server import make_server
2+
from wsgiref.headers import Headers
3+
from wsgiref.validate import validator
4+
5+
def test_app(environ, start_response):
6+
status = "200 OK"
7+
h_name = environ["source_n"]
8+
h_val = environ["source_v"]
9+
headers = [(h_name, "val"), ("name", h_val)]
10+
start_response(status, headers) # GOOD - the application is validated, so headers containing newlines will be rejected.
11+
return [b"Hello"]
12+
13+
def test_app2(environ, start_response):
14+
status = "200 OK"
15+
h_name = environ["source_n"]
16+
h_val = environ["source_v"]
17+
headers = Headers([(h_name, "val"), ("name", h_val)]) # GOOD
18+
headers.add_header(h_name, h_val) # GOOD
19+
headers.setdefault(h_name, h_val) # GOOD
20+
headers.__setitem__(h_name, h_val) # GOOD
21+
headers[h_name] = h_val # GOOD
22+
start_response(status, headers)
23+
return [b"Hello"]
24+
25+
def main1():
26+
with make_server('', 8000, validate(test_app)) as httpd:
27+
print("Serving on port 8000...")
28+
httpd.serve_forever()
29+
30+
def main2():
31+
with make_server('', 8000, validate(test_app2)) as httpd:
32+
print("Serving on port 8000...")
33+
httpd.serve_forever()

0 commit comments

Comments
 (0)