Skip to content

Commit a210391

Browse files
committed
Python: Model (most of) twisted
1 parent 151a733 commit a210391

File tree

6 files changed

+284
-53
lines changed

6 files changed

+284
-53
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ Python built-in support
155155
Django, Web framework
156156
Flask, Web framework
157157
Tornado, Web framework
158+
Twisted, Web framework
158159
PyYAML, Serialization
159160
dill, Serialization
160161
simplejson, Serialization

python/ql/src/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,6 @@ private import semmle.python.frameworks.PyMySQL
1919
private import semmle.python.frameworks.Simplejson
2020
private import semmle.python.frameworks.Stdlib
2121
private import semmle.python.frameworks.Tornado
22+
private import semmle.python.frameworks.Twisted
2223
private import semmle.python.frameworks.Ujson
2324
private import semmle.python.frameworks.Yaml
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `twisted` PyPI package.
3+
* See https://twistedmatrix.com/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.dataflow.new.DataFlow
8+
private import semmle.python.dataflow.new.RemoteFlowSources
9+
private import semmle.python.dataflow.new.TaintTracking
10+
private import semmle.python.Concepts
11+
private import semmle.python.ApiGraphs
12+
13+
/**
14+
* Provides models for the `twisted` PyPI package.
15+
* See https://twistedmatrix.com/.
16+
*/
17+
private module Twisted {
18+
// ---------------------------------------------------------------------------
19+
// request handler modeling
20+
// ---------------------------------------------------------------------------
21+
/**
22+
* A class that is a subclass of `twisted.web.resource.Resource`, thereby
23+
* being able to handle HTTP requests.
24+
*
25+
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.resource.Resource.html
26+
*/
27+
class TwistedResourceSubclass extends Class {
28+
TwistedResourceSubclass() {
29+
this.getABase() =
30+
API::moduleImport("twisted")
31+
.getMember("web")
32+
.getMember("resource")
33+
.getMember("Resource")
34+
.getASubclass*()
35+
.getAUse()
36+
.asExpr()
37+
}
38+
39+
/** Gets a function that could handle incoming requests, if any. */
40+
Function getARequestHandler() {
41+
// TODO: This doesn't handle attribute assignment. Should be OK, but analysis is not as complete as with
42+
// points-to and `.lookup`, which would handle `post = my_post_handler` inside class def
43+
result = this.getAMethod() and
44+
resourceMethodRequestParamIndex(result.getName(), _)
45+
}
46+
}
47+
48+
/**
49+
* Holds if the request parameter is supposed to be at index `requestParamIndex` for
50+
* the method named `methodName` in `twisted.web.resource.Resource`.
51+
*/
52+
bindingset[methodName]
53+
private predicate resourceMethodRequestParamIndex(string methodName, int requestParamIndex) {
54+
methodName.matches("render_%") and requestParamIndex = 1
55+
or
56+
methodName in ["render", "listDynamicEntities", "getChildForRequest"] and requestParamIndex = 1
57+
or
58+
methodName = ["getDynamicEntity", "getChild", "getChildWithDefault"] and requestParamIndex = 2
59+
}
60+
61+
/** A method that handles incoming requests, on a `twisted.web.resource.Resource` subclass. */
62+
class TwistedResourceRequestHandler extends HTTP::Server::RequestHandler::Range {
63+
TwistedResourceRequestHandler() {
64+
any(TwistedResourceSubclass cls).getAMethod() = this and
65+
resourceMethodRequestParamIndex(this.getName(), _)
66+
}
67+
68+
Parameter getRequestParameter() {
69+
exists(int i |
70+
resourceMethodRequestParamIndex(this.getName(), i) and
71+
result = this.getArg(i)
72+
)
73+
}
74+
75+
override Parameter getARoutedParameter() { none() }
76+
77+
override string getFramework() { result = "twisted" }
78+
}
79+
80+
/**
81+
* A "render" method on a `twisted.web.resource.Resource` subclass, whose return value
82+
* is written as the body fo the HTTP response.
83+
*/
84+
class TwistedResourceRenderMethod extends TwistedResourceRequestHandler {
85+
TwistedResourceRenderMethod() {
86+
this.getName() = "render" or this.getName().matches("render_%")
87+
}
88+
}
89+
90+
// ---------------------------------------------------------------------------
91+
// request modeling
92+
// ---------------------------------------------------------------------------
93+
/**
94+
* Provides models for the `twisted.web.server.Request` class
95+
*
96+
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.server.Request.html
97+
*/
98+
module Request {
99+
/**
100+
* A source of instances of `twisted.web.server.Request`, extend this class to model new instances.
101+
*
102+
* This can include instantiations of the class, return values from function
103+
* calls, or a special parameter that will be set when functions are called by an external
104+
* library.
105+
*
106+
* Use `Request::instance()` predicate to get
107+
* references to instances of `twisted.web.server.Request`.
108+
*/
109+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
110+
111+
/** Gets a reference to an instance of `twisted.web.server.Request`. */
112+
private DataFlow::LocalSourceNode instance(DataFlow::TypeTracker t) {
113+
t.start() and
114+
result instanceof InstanceSource
115+
or
116+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
117+
}
118+
119+
/** Gets a reference to an instance of `twisted.web.server.Request`. */
120+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
121+
}
122+
123+
/**
124+
* A parameter that will receive a `twisted.web.server.Request` instance,
125+
* when a twisted request handler is called.
126+
*/
127+
class TwistedResourceRequestHandlerRequestParam extends RemoteFlowSource::Range,
128+
Request::InstanceSource, DataFlow::ParameterNode {
129+
TwistedResourceRequestHandlerRequestParam() {
130+
this.getParameter() = any(TwistedResourceRequestHandler handler).getRequestParameter()
131+
}
132+
133+
override string getSourceType() { result = "twisted.web.server.Request" }
134+
}
135+
136+
/**
137+
* Taint propagation for `twisted.web.server.Request`.
138+
*/
139+
private class TwistedRequestAdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
140+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
141+
// Methods
142+
//
143+
// TODO: When we have tools that make it easy, model these properly to handle
144+
// `meth = obj.meth; meth()`. Until then, we'll use this more syntactic approach
145+
// (since it allows us to at least capture the most common cases).
146+
nodeFrom = Request::instance() and
147+
exists(DataFlow::AttrRead attr | attr.getObject() = nodeFrom |
148+
// normal (non-async) methods
149+
attr.getAttributeName() in [
150+
"getCookie", "getHeader", "getAllHeaders", "getUser", "getPassword", "getHost",
151+
"getRequestHostname"
152+
] and
153+
nodeTo.(DataFlow::CallCfgNode).getFunction() = attr
154+
)
155+
or
156+
// Attributes
157+
nodeFrom = Request::instance() and
158+
nodeTo.(DataFlow::AttrRead).getObject() = nodeFrom and
159+
nodeTo.(DataFlow::AttrRead).getAttributeName() in [
160+
"uri", "path", "prepath", "postpath", "content", "args", "received_cookies",
161+
"requestHeaders", "user", "password", "host"
162+
]
163+
}
164+
}
165+
166+
/**
167+
* A parameter of a request handler method (on a `twisted.web.resource.Resource` subclass)
168+
* that is also given remote user input. (a bit like RoutedParameter).
169+
*/
170+
class TwistedResourceRequestHandlerExtraSources extends RemoteFlowSource::Range,
171+
DataFlow::ParameterNode {
172+
TwistedResourceRequestHandlerExtraSources() {
173+
exists(TwistedResourceRequestHandler func, int i |
174+
func.getName() in ["getChild", "getChildWithDefault"] and i = 1
175+
or
176+
func.getName() = "getDynamicEntity" and i = 1
177+
|
178+
this.getParameter() = func.getArg(i)
179+
)
180+
}
181+
182+
override string getSourceType() { result = "twisted Resource method extra parameter" }
183+
}
184+
185+
// ---------------------------------------------------------------------------
186+
// response modeling
187+
// ---------------------------------------------------------------------------
188+
/**
189+
* Implicit response from returns of render methods.
190+
*/
191+
private class TwistedResourceRenderMethodReturn extends HTTP::Server::HttpResponse::Range,
192+
DataFlow::CfgNode {
193+
TwistedResourceRenderMethodReturn() {
194+
this.asCfgNode() = any(TwistedResourceRenderMethod meth).getAReturnValueFlowNode()
195+
}
196+
197+
override DataFlow::Node getBody() { result = this }
198+
199+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
200+
201+
override string getMimetypeDefault() { result = "text/html" }
202+
}
203+
204+
/**
205+
* A call to the `twisted.web.server.Request.write` function.
206+
*
207+
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.server.Request.html#write
208+
*/
209+
class TwistedRequestWriteCall extends HTTP::Server::HttpResponse::Range, DataFlow::CallCfgNode {
210+
TwistedRequestWriteCall() {
211+
// TODO: When we have tools that make it easy, model these properly to handle
212+
// `meth = obj.meth; meth()`. Until then, we'll use this more syntactic approach
213+
// (since it allows us to at least capture the most common cases).
214+
exists(DataFlow::AttrRead read |
215+
this.getFunction() = read and
216+
read.getObject() = Request::instance() and
217+
read.getAttributeName() = "write"
218+
)
219+
}
220+
221+
override DataFlow::Node getBody() {
222+
result.asCfgNode() in [node.getArg(0), node.getArgByName("data")]
223+
}
224+
225+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
226+
227+
override string getMimetypeDefault() { result = "text/html" }
228+
}
229+
}

python/ql/test/library-tests/frameworks/twisted/response_test.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,55 +6,55 @@
66
root = Resource()
77

88
class Now(Resource):
9-
def render(self, request: Request):
10-
return b"now"
9+
def render(self, request: Request): # $ requestHandler
10+
return b"now" # $ HttpResponse mimetype=text/html responseBody=b"now"
1111

1212

1313
class AlsoNow(Resource):
14-
def render(self, request: Request):
15-
request.write(b"also now")
16-
return b""
14+
def render(self, request: Request): # $ requestHandler
15+
request.write(b"also now") # $ HttpResponse mimetype=text/html responseBody=b"also now"
16+
return b"" # $ HttpResponse mimetype=text/html responseBody=b""
1717

1818

1919
def process_later(request: Request):
2020
print("process_later called")
21-
request.write(b"later")
21+
request.write(b"later") # $ MISSING: responseBody=b"later"
2222
request.finish()
2323

2424

2525
class Later(Resource):
26-
def render(self, request: Request):
26+
def render(self, request: Request): # $ requestHandler
2727
# process the request in 1 second
2828
print("setting up callback for process_later")
2929
reactor.callLater(1, process_later, request)
30-
return NOT_DONE_YET
30+
return NOT_DONE_YET # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=NOT_DONE_YET
3131

3232

3333
class PlainText(Resource):
34-
def render(self, request: Request):
34+
def render(self, request: Request): # $ requestHandler
3535
request.setHeader(b"content-type", "text/plain")
36-
return b"this is plain text"
36+
return b"this is plain text" # $ HttpResponse responseBody=b"this is plain text" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain
3737

3838

3939
class Redirect(Resource):
40-
def render_GET(self, request: Request):
41-
request.redirect("/new-location")
40+
def render_GET(self, request: Request): # $ requestHandler
41+
request.redirect("/new-location") # $ MISSING: HttpRedirectResponse
4242
# By default, this `hello` output is not returned... not even when
4343
# requested with curl.
44-
return b"hello"
44+
return b"hello" # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=b"hello"
4545

4646

4747
class NonHttpBodyOutput(Resource):
4848
"""Examples of provides values in response that is not in the body
4949
"""
50-
def render_GET(self, request: Request):
50+
def render_GET(self, request: Request): # $ requestHandler
5151
request.responseHeaders.addRawHeader("key", "value")
5252
request.setHeader("key2", "value")
5353

5454
request.addCookie("key", "value")
5555
request.cookies.append(b"key2=value")
5656

57-
return b""
57+
return b"" # $ HttpResponse mimetype=text/html responseBody=b""
5858

5959

6060
root.putChild(b"now", Now())

python/ql/test/library-tests/frameworks/twisted/routing_test.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,11 @@
77

88

99
class Foo(Resource):
10-
def render(self, request: Request):
10+
def render(self, request: Request): # $ requestHandler
1111
print(f"{request.content=}")
1212
print(f"{request.cookies=}")
1313
print(f"{request.received_cookies=}")
14-
return b"I am Foo"
14+
return b"I am Foo" # $ HttpResponse
1515

1616

1717
root.putChild(b"foo", Foo())
@@ -21,17 +21,17 @@ class Child(Resource):
2121
def __init__(self, name):
2222
self.name = name.decode("utf-8")
2323

24-
def render_GET(self, request):
25-
return f"Hi, I'm child '{self.name}'".encode("utf-8")
24+
def render_GET(self, request): # $ requestHandler
25+
return f"Hi, I'm child '{self.name}'".encode("utf-8") # $ HttpResponse
2626

2727

2828
class Parent(Resource):
29-
def getChild(self, path, request):
29+
def getChild(self, path, request): # $ requestHandler
3030
print(path, type(path))
3131
return Child(path)
3232

33-
def render_GET(self, request):
34-
return b"Hi, I'm parent"
33+
def render_GET(self, request): # $ requestHandler
34+
return b"Hi, I'm parent" # $ HttpResponse
3535

3636

3737
root.putChild(b"parent", Parent())

0 commit comments

Comments
 (0)