Skip to content

Commit 6eb4525

Browse files
committed
Python: Model wsgiref.simple_server applications
1 parent e812029 commit 6eb4525

File tree

3 files changed

+167
-8
lines changed

3 files changed

+167
-8
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
lgtm,codescanning
2+
* Added modeling of `wsgiref.simple_server` applications, leading to new remote flow sources.

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

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ private import semmle.python.dataflow.new.RemoteFlowSources
1010
private import semmle.python.Concepts
1111
private import semmle.python.ApiGraphs
1212
private import semmle.python.frameworks.PEP249
13+
private import semmle.python.frameworks.internal.PoorMansFunctionResolution
14+
private import semmle.python.frameworks.internal.SelfRefMixin
1315
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
1416

1517
/** Provides models for the Python standard library. */
@@ -1316,6 +1318,161 @@ private module StdlibPrivate {
13161318
}
13171319
}
13181320

1321+
// ---------------------------------------------------------------------------
1322+
// wsgiref.simple_server
1323+
// ---------------------------------------------------------------------------
1324+
/** Provides models for the `wsgiref.simple_server` module. */
1325+
private module WsgirefSimpleServer {
1326+
class WsgiServerSubclass extends Class, SelfRefMixin {
1327+
WsgiServerSubclass() {
1328+
this.getABase() =
1329+
API::moduleImport("wsgiref")
1330+
.getMember("simple_server")
1331+
.getMember("WSGIServer")
1332+
.getASubclass*()
1333+
.getAUse()
1334+
.asExpr()
1335+
}
1336+
}
1337+
1338+
/**
1339+
* A function that was passed to the `set_app` method of a
1340+
* `wsgiref.simple_server.WSGIServer` instance.
1341+
*
1342+
* See https://docs.python.org/3.10/library/wsgiref.html#wsgiref.simple_server.WSGIServer.set_app
1343+
*
1344+
* See https://github.com/python/cpython/blob/b567b9d74bd9e476a3027335873bb0508d6e450f/Lib/wsgiref/handlers.py#L137
1345+
* for how a request is processed and given to an application.
1346+
*/
1347+
class WsgirefSimpleServerApplication extends HTTP::Server::RequestHandler::Range {
1348+
WsgirefSimpleServerApplication() {
1349+
exists(DataFlow::Node appArg, DataFlow::CallCfgNode setAppCall |
1350+
(
1351+
setAppCall =
1352+
API::moduleImport("wsgiref")
1353+
.getMember("simple_server")
1354+
.getMember("WSGIServer")
1355+
.getASubclass*()
1356+
.getReturn()
1357+
.getMember("set_app")
1358+
.getACall()
1359+
or
1360+
setAppCall
1361+
.(DataFlow::MethodCallNode)
1362+
.calls(any(WsgiServerSubclass cls).getASelfRef(), "set_app")
1363+
) and
1364+
appArg in [setAppCall.getArg(0), setAppCall.getArgByName("application")]
1365+
|
1366+
appArg = poorMansFunctionTracker(this)
1367+
)
1368+
}
1369+
1370+
override Parameter getARoutedParameter() { none() }
1371+
1372+
override string getFramework() { result = "Stdlib: wsgiref.simple_server application" }
1373+
}
1374+
1375+
/**
1376+
* The parameter of a `WsgirefSimpleServerApplication` that takes the WSGI environment
1377+
* when processing a request.
1378+
*
1379+
* See https://docs.python.org/3.10/library/wsgiref.html#wsgiref.simple_server.WSGIRequestHandler.get_environ
1380+
*/
1381+
class WSGIEnvirontParameter extends RemoteFlowSource::Range, DataFlow::ParameterNode {
1382+
WSGIEnvirontParameter() {
1383+
exists(WsgirefSimpleServerApplication func |
1384+
if func.isMethod()
1385+
then this.getParameter() = func.getArg(1)
1386+
else this.getParameter() = func.getArg(0)
1387+
)
1388+
}
1389+
1390+
override string getSourceType() {
1391+
result = "Stdlib: wsgiref.simple_server application: WSGI environment parameter"
1392+
}
1393+
}
1394+
1395+
/**
1396+
* Gets a reference to the parameter of a `WsgirefSimpleServerApplication` that
1397+
* takes the `start_response` function.
1398+
*
1399+
* See https://github.com/python/cpython/blob/b567b9d74bd9e476a3027335873bb0508d6e450f/Lib/wsgiref/handlers.py#L225-L252
1400+
*/
1401+
private DataFlow::TypeTrackingNode startResponse(DataFlow::TypeTracker t) {
1402+
t.start() and
1403+
exists(WsgirefSimpleServerApplication func |
1404+
if func.isMethod()
1405+
then result.(DataFlow::ParameterNode).getParameter() = func.getArg(2)
1406+
else result.(DataFlow::ParameterNode).getParameter() = func.getArg(1)
1407+
)
1408+
or
1409+
exists(DataFlow::TypeTracker t2 | result = startResponse(t2).track(t2, t))
1410+
}
1411+
1412+
/**
1413+
* Gets a reference to the parameter of a `WsgirefSimpleServerApplication` that
1414+
* takes the `start_response` function.
1415+
*
1416+
* See https://github.com/python/cpython/blob/b567b9d74bd9e476a3027335873bb0508d6e450f/Lib/wsgiref/handlers.py#L225-L252
1417+
*/
1418+
DataFlow::Node startResponse() { startResponse(DataFlow::TypeTracker::end()).flowsTo(result) }
1419+
1420+
/**
1421+
* Gets a reference to the `write` function (that will write data to the response),
1422+
* which is the return value from calling the `start_response` function.
1423+
*
1424+
* See https://github.com/python/cpython/blob/b567b9d74bd9e476a3027335873bb0508d6e450f/Lib/wsgiref/handlers.py#L225-L252
1425+
*/
1426+
private DataFlow::TypeTrackingNode writeFunction(DataFlow::TypeTracker t) {
1427+
t.start() and
1428+
result.(DataFlow::CallCfgNode).getFunction() = startResponse()
1429+
or
1430+
exists(DataFlow::TypeTracker t2 | result = writeFunction(t2).track(t2, t))
1431+
}
1432+
1433+
/**
1434+
* Gets a reference to the `write` function (that will write data to the response),
1435+
* which is the return value from calling the `start_response` function.
1436+
*
1437+
* See https://github.com/python/cpython/blob/b567b9d74bd9e476a3027335873bb0508d6e450f/Lib/wsgiref/handlers.py#L225-L252
1438+
*/
1439+
DataFlow::Node writeFunction() { writeFunction(DataFlow::TypeTracker::end()).flowsTo(result) }
1440+
1441+
/**
1442+
* A call to the `write` function.
1443+
*
1444+
* See https://github.com/python/cpython/blob/b567b9d74bd9e476a3027335873bb0508d6e450f/Lib/wsgiref/handlers.py#L276
1445+
*/
1446+
class WsgirefSimpleServerApplicationWriteCall extends HTTP::Server::HttpResponse::Range,
1447+
DataFlow::CallCfgNode {
1448+
WsgirefSimpleServerApplicationWriteCall() { this.getFunction() = writeFunction() }
1449+
1450+
override DataFlow::Node getBody() { result in [this.getArg(0), this.getArgByName("data")] }
1451+
1452+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
1453+
1454+
override string getMimetypeDefault() { none() }
1455+
}
1456+
1457+
/**
1458+
* A return from a `WsgirefSimpleServerApplication`, which is included in the response body.
1459+
*/
1460+
class WsgirefSimpleServerApplicationReturn extends HTTP::Server::HttpResponse::Range,
1461+
DataFlow::CfgNode {
1462+
WsgirefSimpleServerApplicationReturn() {
1463+
exists(WsgirefSimpleServerApplication requestHandler |
1464+
node = requestHandler.getAReturnValueFlowNode()
1465+
)
1466+
}
1467+
1468+
override DataFlow::Node getBody() { result = this }
1469+
1470+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
1471+
1472+
override string getMimetypeDefault() { none() }
1473+
}
1474+
}
1475+
13191476
// ---------------------------------------------------------------------------
13201477
// sqlite3
13211478
// ---------------------------------------------------------------------------

python/ql/test/library-tests/frameworks/stdlib/wsgiref_simple_server_test.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,18 @@ def ignore(*arg, **kwargs): pass
1212
# I wanted to showcase that we handle both functions and bound-methods, so it's possible
1313
# to run this test-file in 2 different ways.
1414

15-
def func(environ, start_response): # $ MISSING: requestHandler
15+
def func(environ, start_response): # $ requestHandler
1616
ensure_tainted(
17-
environ, # $ MISSING: tainted
18-
environ["PATH_INFO"], # $ MISSING: tainted
17+
environ, # $ tainted
18+
environ["PATH_INFO"], # $ tainted
1919
)
2020
write = start_response("200 OK", [("Content-Type", "text/plain")])
21-
write(b"hello") # $ MISSING: HttpResponse responseBody=b"hello"
22-
write(data=b" ") # $ MISSING: HttpResponse responseBody=b" "
21+
write(b"hello") # $ HttpResponse responseBody=b"hello"
22+
write(data=b" ") # $ HttpResponse responseBody=b" "
2323

2424
# function return value should be an iterable that will also be written to to the
2525
# response.
26-
return [b"world", b"!"] # $ MISSING: HttpResponse responseBody=List
26+
return [b"world", b"!"] # $ HttpResponse responseBody=List
2727

2828

2929
class MyServer(wsgiref.simple_server.WSGIServer):
@@ -44,9 +44,9 @@ def my_method(self, _env, start_response): # $ MISSING: requestHandler
4444
server = MyServer()
4545
elif case == "3":
4646
server = MyServer()
47-
def func3(_env, start_response): # $ MISSING: requestHandler
47+
def func3(_env, start_response): # $ requestHandler
4848
start_response("200 OK", [])
49-
return [b"foo"] # $ MISSING: HttpResponse responseBody=List
49+
return [b"foo"] # $ HttpResponse responseBody=List
5050
server.set_app(func3)
5151
else:
5252
sys.exit("wrong case")

0 commit comments

Comments
 (0)