Skip to content

Commit baf8d0a

Browse files
authored
Merge pull request github#6045 from RasmusWL/twisted
Python: Model twisted
2 parents 14b485e + d6ec4d3 commit baf8d0a

File tree

11 files changed

+462
-0
lines changed

11 files changed

+462
-0
lines changed

docs/codeql/support/reusables/frameworks.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,7 @@ Python built-in support
156156
Django, Web framework
157157
Flask, Web framework
158158
Tornado, Web framework
159+
Twisted, Web framework
159160
PyYAML, Serialization
160161
dill, Serialization
161162
simplejson, Serialization
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 sources/sinks when using `twisted` to create web servers.

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ private import semmle.python.frameworks.PyMySQL
2323
private import semmle.python.frameworks.Simplejson
2424
private import semmle.python.frameworks.Stdlib
2525
private import semmle.python.frameworks.Tornado
26+
private import semmle.python.frameworks.Twisted
2627
private import semmle.python.frameworks.Ujson
2728
private import semmle.python.frameworks.Yaml
2829
private import semmle.python.frameworks.Yarl
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
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+
exists(getRequestParamIndex(result.getName()))
45+
}
46+
}
47+
48+
/**
49+
* Gets the index the request parameter is supposed to be at for the method named
50+
* `methodName` in a `twisted.web.resource.Resource` subclass.
51+
*/
52+
bindingset[methodName]
53+
private int getRequestParamIndex(string methodName) {
54+
methodName.matches("render_%") and result = 1
55+
or
56+
methodName in ["render", "listDynamicEntities", "getChildForRequest"] and result = 1
57+
or
58+
methodName = ["getDynamicEntity", "getChild", "getChildWithDefault"] and result = 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() { this = any(TwistedResourceSubclass cls).getARequestHandler() }
64+
65+
Parameter getRequestParameter() { result = this.getArg(getRequestParamIndex(this.getName())) }
66+
67+
override Parameter getARoutedParameter() { none() }
68+
69+
override string getFramework() { result = "twisted" }
70+
}
71+
72+
/**
73+
* A "render" method on a `twisted.web.resource.Resource` subclass, whose return value
74+
* is written as the body of the HTTP response.
75+
*/
76+
class TwistedResourceRenderMethod extends TwistedResourceRequestHandler {
77+
TwistedResourceRenderMethod() {
78+
this.getName() = "render" or this.getName().matches("render_%")
79+
}
80+
}
81+
82+
// ---------------------------------------------------------------------------
83+
// request modeling
84+
// ---------------------------------------------------------------------------
85+
/**
86+
* Provides models for the `twisted.web.server.Request` class
87+
*
88+
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.server.Request.html
89+
*/
90+
module Request {
91+
/**
92+
* A source of instances of `twisted.web.server.Request`, extend this class to model new instances.
93+
*
94+
* This can include instantiations of the class, return values from function
95+
* calls, or a special parameter that will be set when functions are called by an external
96+
* library.
97+
*
98+
* Use `Request::instance()` predicate to get
99+
* references to instances of `twisted.web.server.Request`.
100+
*/
101+
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
102+
103+
/** Gets a reference to an instance of `twisted.web.server.Request`. */
104+
private DataFlow::LocalSourceNode instance(DataFlow::TypeTracker t) {
105+
t.start() and
106+
result instanceof InstanceSource
107+
or
108+
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
109+
}
110+
111+
/** Gets a reference to an instance of `twisted.web.server.Request`. */
112+
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
113+
}
114+
115+
/**
116+
* A parameter that will receive a `twisted.web.server.Request` instance,
117+
* when a twisted request handler is called.
118+
*/
119+
class TwistedResourceRequestHandlerRequestParam extends RemoteFlowSource::Range,
120+
Request::InstanceSource, DataFlow::ParameterNode {
121+
TwistedResourceRequestHandlerRequestParam() {
122+
this.getParameter() = any(TwistedResourceRequestHandler handler).getRequestParameter()
123+
}
124+
125+
override string getSourceType() { result = "twisted.web.server.Request" }
126+
}
127+
128+
/**
129+
* Taint propagation for `twisted.web.server.Request`.
130+
*/
131+
private class TwistedRequestAdditionalTaintStep extends TaintTracking::AdditionalTaintStep {
132+
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
133+
// Methods
134+
//
135+
// TODO: When we have tools that make it easy, model these properly to handle
136+
// `meth = obj.meth; meth()`. Until then, we'll use this more syntactic approach
137+
// (since it allows us to at least capture the most common cases).
138+
nodeFrom = Request::instance() and
139+
exists(DataFlow::AttrRead attr | attr.getObject() = nodeFrom |
140+
// normal (non-async) methods
141+
attr.getAttributeName() in [
142+
"getCookie", "getHeader", "getAllHeaders", "getUser", "getPassword", "getHost",
143+
"getRequestHostname"
144+
] and
145+
nodeTo.(DataFlow::CallCfgNode).getFunction() = attr
146+
)
147+
or
148+
// Attributes
149+
nodeFrom = Request::instance() and
150+
nodeTo.(DataFlow::AttrRead).getObject() = nodeFrom and
151+
nodeTo.(DataFlow::AttrRead).getAttributeName() in [
152+
"uri", "path", "prepath", "postpath", "content", "args", "received_cookies",
153+
"requestHeaders", "user", "password", "host"
154+
]
155+
}
156+
}
157+
158+
/**
159+
* A parameter of a request handler method (on a `twisted.web.resource.Resource` subclass)
160+
* that is also given remote user input. (a bit like RoutedParameter).
161+
*/
162+
class TwistedResourceRequestHandlerExtraSources extends RemoteFlowSource::Range,
163+
DataFlow::ParameterNode {
164+
TwistedResourceRequestHandlerExtraSources() {
165+
exists(TwistedResourceRequestHandler func, int i |
166+
func.getName() in ["getChild", "getChildWithDefault"] and i = 1
167+
or
168+
func.getName() = "getDynamicEntity" and i = 1
169+
|
170+
this.getParameter() = func.getArg(i)
171+
)
172+
}
173+
174+
override string getSourceType() { result = "twisted Resource method extra parameter" }
175+
}
176+
177+
// ---------------------------------------------------------------------------
178+
// response modeling
179+
// ---------------------------------------------------------------------------
180+
/**
181+
* Implicit response from returns of render methods.
182+
*/
183+
private class TwistedResourceRenderMethodReturn extends HTTP::Server::HttpResponse::Range,
184+
DataFlow::CfgNode {
185+
TwistedResourceRenderMethodReturn() {
186+
this.asCfgNode() = any(TwistedResourceRenderMethod meth).getAReturnValueFlowNode()
187+
}
188+
189+
override DataFlow::Node getBody() { result = this }
190+
191+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
192+
193+
override string getMimetypeDefault() { result = "text/html" }
194+
}
195+
196+
/**
197+
* A call to the `twisted.web.server.Request.write` function.
198+
*
199+
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.server.Request.html#write
200+
*/
201+
class TwistedRequestWriteCall extends HTTP::Server::HttpResponse::Range, DataFlow::CallCfgNode {
202+
TwistedRequestWriteCall() {
203+
// TODO: When we have tools that make it easy, model these properly to handle
204+
// `meth = obj.meth; meth()`. Until then, we'll use this more syntactic approach
205+
// (since it allows us to at least capture the most common cases).
206+
exists(DataFlow::AttrRead read |
207+
this.getFunction() = read and
208+
read.getObject() = Request::instance() and
209+
read.getAttributeName() = "write"
210+
)
211+
}
212+
213+
override DataFlow::Node getBody() {
214+
result.asCfgNode() in [node.getArg(0), node.getArgByName("data")]
215+
}
216+
217+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
218+
219+
override string getMimetypeDefault() { result = "text/html" }
220+
}
221+
222+
/**
223+
* A call to the `redirect` function on a twisted request.
224+
*
225+
* See https://twistedmatrix.com/documents/21.2.0/api/twisted.web.http.Request.html#redirect
226+
*/
227+
class TwistedRequestRedirectCall extends HTTP::Server::HttpRedirectResponse::Range,
228+
DataFlow::CallCfgNode {
229+
TwistedRequestRedirectCall() {
230+
// TODO: When we have tools that make it easy, model these properly to handle
231+
// `meth = obj.meth; meth()`. Until then, we'll use this more syntactic approach
232+
// (since it allows us to at least capture the most common cases).
233+
exists(DataFlow::AttrRead read |
234+
this.getFunction() = read and
235+
read.getObject() = Request::instance() and
236+
read.getAttributeName() = "redirect"
237+
)
238+
}
239+
240+
override DataFlow::Node getBody() { none() }
241+
242+
override DataFlow::Node getRedirectLocation() {
243+
result.asCfgNode() in [node.getArg(0), node.getArgByName("url")]
244+
}
245+
246+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
247+
248+
override string getMimetypeDefault() { result = "text/html" }
249+
}
250+
}

python/ql/test/library-tests/frameworks/twisted/ConceptsTest.expected

Whitespace-only changes.
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import python
2+
import experimental.meta.ConceptsTest
3+
4+
class DedicatedResponseTest extends HttpServerHttpResponseTest {
5+
DedicatedResponseTest() { file.getShortName() = "response_test.py" }
6+
}
7+
8+
class OtherResponseTest extends HttpServerHttpResponseTest {
9+
OtherResponseTest() { not this instanceof DedicatedResponseTest }
10+
11+
override string getARelevantTag() { result = "HttpResponse" }
12+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
argumentToEnsureNotTaintedNotMarkedAsSpurious
2+
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
3+
failures
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import experimental.meta.InlineTaintTest
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
from twisted.web.server import Site, Request, NOT_DONE_YET
2+
from twisted.web.resource import Resource
3+
from twisted.internet import reactor, endpoints, defer
4+
5+
6+
root = Resource()
7+
8+
class Now(Resource):
9+
def render(self, request: Request): # $ requestHandler
10+
return b"now" # $ HttpResponse mimetype=text/html responseBody=b"now"
11+
12+
13+
class AlsoNow(Resource):
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""
17+
18+
19+
def process_later(request: Request):
20+
print("process_later called")
21+
request.write(b"later") # $ MISSING: responseBody=b"later"
22+
request.finish()
23+
24+
25+
class Later(Resource):
26+
def render(self, request: Request): # $ requestHandler
27+
# process the request in 1 second
28+
print("setting up callback for process_later")
29+
reactor.callLater(1, process_later, request)
30+
return NOT_DONE_YET # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=NOT_DONE_YET
31+
32+
33+
class PlainText(Resource):
34+
def render(self, request: Request): # $ requestHandler
35+
request.setHeader(b"content-type", "text/plain")
36+
return b"this is plain text" # $ HttpResponse responseBody=b"this is plain text" SPURIOUS: mimetype=text/html MISSING: mimetype=text/plain
37+
38+
39+
class Redirect(Resource):
40+
def render_GET(self, request: Request): # $ requestHandler
41+
request.redirect("/new-location") # $ HttpRedirectResponse redirectLocation="/new-location" HttpResponse mimetype=text/html
42+
# By default, this `hello` output is not returned... not even when
43+
# requested with curl.
44+
return b"hello" # $ SPURIOUS: HttpResponse mimetype=text/html responseBody=b"hello"
45+
46+
47+
class NonHttpBodyOutput(Resource):
48+
"""Examples of providing values in response that is not in the body
49+
"""
50+
def render_GET(self, request: Request): # $ requestHandler
51+
request.responseHeaders.addRawHeader("key", "value")
52+
request.setHeader("key2", "value")
53+
54+
request.addCookie("key", "value")
55+
request.cookies.append(b"key2=value")
56+
57+
return b"" # $ HttpResponse mimetype=text/html responseBody=b""
58+
59+
60+
root.putChild(b"now", Now())
61+
root.putChild(b"also-now", AlsoNow())
62+
root.putChild(b"later", Later())
63+
root.putChild(b"plain-text", PlainText())
64+
root.putChild(b"redirect", Redirect())
65+
root.putChild(b"non-body", NonHttpBodyOutput())
66+
67+
68+
if __name__ == "__main__":
69+
factory = Site(root)
70+
endpoint = endpoints.TCP4ServerEndpoint(reactor, 8880)
71+
endpoint.listen(factory)
72+
73+
print("Will run on http://localhost:8880")
74+
75+
reactor.run()

0 commit comments

Comments
 (0)