Skip to content

Commit 7df8b1b

Browse files
Don't rely on specific parameter names, add qldoc
1 parent 2a04598 commit 7df8b1b

File tree

2 files changed

+111
-39
lines changed

2 files changed

+111
-39
lines changed

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

Lines changed: 103 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -19,49 +19,62 @@ private import semmle.python.frameworks.Stdlib
1919
* See https://docs.pylonsproject.org/projects/pyramid/.
2020
*/
2121
module Pyramid {
22-
// TODO: qldoc
22+
/** Provides models for pyramid View callables. */
2323
module View {
2424
/**
2525
* A callable that could be used as a pyramid view callable.
2626
*/
2727
private class PotentialViewCallable extends Function {
28-
PotentialViewCallable() {
28+
PotentialViewCallable() { this.getPositionalParameterCount() in [1, 2] }
29+
30+
/** Gets the `request` parameter of this view callable. */
31+
Parameter getRequestParameter() {
2932
this.getPositionalParameterCount() = 1 and
30-
this.getArgName(0) = "request"
33+
result = this.getArg(0)
3134
or
3235
this.getPositionalParameterCount() = 2 and
33-
this.getArgName(0) = "context" and
34-
this.getArgName(1) = "request"
36+
result = this.getArg(1)
3537
}
36-
37-
/** Gets the `request` parameter of this view callable. */
38-
Parameter getRequestParameter() { result = this.getArgByName("request") }
3938
}
4039

41-
abstract class ViewCallable extends PotentialViewCallable, Http::Server::RequestHandler::Range {
42-
override Parameter getARoutedParameter() { result = this.getRequestParameter() }
43-
40+
/** A dataflow node that sets up a route on a server using the Pyramid framework. */
41+
abstract private class PyramidRouteSetup extends Http::Server::RouteSetup::Range {
4442
override string getFramework() { result = "Pyramid" }
4543
}
4644

47-
private class ViewCallableFromDecorator extends ViewCallable {
48-
ViewCallableFromDecorator() {
49-
this.getADecorator() =
50-
API::moduleImport("pyramid")
51-
.getMember("view")
52-
.getMember("view_config")
53-
.getACall()
54-
.asExpr()
45+
/**
46+
* A Pyramid view callable, that handles incoming requests.
47+
*/
48+
class ViewCallable extends PotentialViewCallable {
49+
ViewCallable() { this = any(PyramidRouteSetup rs).getARequestHandler() }
50+
}
51+
52+
/** A pyramid route setup using the `pyramid.view.view_config` decorator. */
53+
private class DecoratorSetup extends PyramidRouteSetup {
54+
DecoratorSetup() {
55+
this = API::moduleImport("pyramid").getMember("view").getMember("view_config").getACall()
5556
}
57+
58+
override Function getARequestHandler() { result.getADecorator() = this.asExpr() }
59+
60+
override DataFlow::Node getUrlPatternArg() { none() } // there is a `route_name` arg, but that does not contain the url pattern
61+
62+
override Parameter getARoutedParameter() { none() }
5663
}
5764

58-
private class ViewCallableFromConfigurator extends ViewCallable {
59-
ViewCallableFromConfigurator() {
60-
any(Configurator::AddViewCall c).getViewArg() = poorMansFunctionTracker(this)
65+
/** A pyramid route setup using a call to `pyramid.config.Configurator.add_view`. */
66+
private class ConfiguratorSetup extends PyramidRouteSetup instanceof Configurator::AddViewCall {
67+
override Function getARequestHandler() {
68+
this.(Configurator::AddViewCall).getViewArg() = poorMansFunctionTracker(result)
6169
}
70+
71+
override DataFlow::Node getUrlPatternArg() { none() } // there is a `route_name` arg, but that does not contain the url pattern
72+
73+
override Parameter getARoutedParameter() { none() }
6274
}
6375
}
6476

77+
/** Provides models for `pyramid.config.Configurator` */
6578
module Configurator {
6679
/** Gets a reference to the class `pyramid.config.Configurator`. */
6780
API::Node classRef() {
@@ -79,14 +92,21 @@ module Pyramid {
7992
/** Gets a reference to an instance of `pyramid.config.Configurator`. */
8093
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
8194

95+
/** Gets a call to the `add_view` method of an instance of `pyramid.config.Configurator`. */
8296
class AddViewCall extends DataFlow::MethodCallNode {
8397
AddViewCall() { this.calls(instance(), "add_view") }
8498

8599
DataFlow::Node getViewArg() { result = [this.getArg(0), this.getArgByName("view")] }
86100
}
87101
}
88102

103+
/** Provides modelling for pyramid requests. */
89104
module Request {
105+
/**
106+
* A source of instances of `pyramid.request.Request`, extend this class to model new instances.
107+
*
108+
* Use the predicate `Request::instance()` to get references to instances of `pyramid.request.Request`.
109+
*/
90110
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
91111

92112
/** Gets a reference to an instance of `pyramid.request.Request`. */
@@ -100,10 +120,14 @@ module Pyramid {
100120
/** Gets a reference to an instance of `pyramid.request.Request`. */
101121
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
102122

103-
private class RequestParameter extends InstanceSource, DataFlow::ParameterNode {
123+
private class RequestParameter extends InstanceSource, RemoteFlowSource::Range instanceof DataFlow::ParameterNode
124+
{
104125
RequestParameter() { this.getParameter() = any(View::ViewCallable vc).getRequestParameter() }
126+
127+
override string getSourceType() { result = "Pyramid request parameter" }
105128
}
106129

130+
/** Taint steps for request instances. */
107131
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
108132
InstanceTaintSteps() { this = "pyramid.request.Request" }
109133

@@ -115,9 +139,9 @@ module Pyramid {
115139
"as_bytes", "authorization", "body", "body_file", "body_file_raw", "body_file_seekable",
116140
"cache_control", "client_addr", "content_type", "cookies", "domain", "headers", "host",
117141
"host_port", "host_url", "GET", "if_match", "if_none_match", "if_range",
118-
"if_none_match", "json", "json_body", "params", "path", "path_info", "path_qs",
119-
"path_url", "POST", "pragma", "query_string", "range", "referer", "referrer", "text",
120-
"url", "urlargs", "urlvars", "user_agent"
142+
"if_none_match", "json", "json_body", "matchdict", "params", "path", "path_info",
143+
"path_qs", "path_url", "POST", "pragma", "query_string", "range", "referer", "referrer",
144+
"text", "url", "urlargs", "urlvars", "user_agent"
121145
]
122146
}
123147

@@ -128,10 +152,12 @@ module Pyramid {
128152
override string getAsyncMethodName() { none() }
129153
}
130154

155+
/** A call to a method of a `request` that copies the request. */
131156
private class RequestCopyCall extends InstanceSource, DataFlow::MethodCallNode {
132157
RequestCopyCall() { this.calls(instance(), ["copy", "copy_get"]) }
133158
}
134159

160+
/** A member of a request that is a file-like object. */
135161
private class RequestBodyFileLike extends Stdlib::FileLikeObject::InstanceSource instanceof DataFlow::AttrRead
136162
{
137163
RequestBodyFileLike() {
@@ -141,7 +167,9 @@ module Pyramid {
141167
}
142168
}
143169

170+
/** Provides modelling for pyramid responses. */
144171
module Response {
172+
/** A response returned by a view callable. */
145173
private class PyramidReturnResponse extends Http::Server::HttpResponse::Range {
146174
PyramidReturnResponse() {
147175
this.asCfgNode() = any(View::ViewCallable vc).getAReturnValueFlowNode() and
@@ -155,27 +183,39 @@ module Pyramid {
155183
override string getMimetypeDefault() { result = "text/html" }
156184
}
157185

158-
private API::Node classRef() {
159-
result = API::moduleImport("pyramid").getMember("response").getMember("Response")
186+
/** Gets a reference to the class `pyramid.response.Response` or a subclass. */
187+
API::Node subclassRef() {
188+
result = API::moduleImport("pyramid").getMember("response").getMember("Response") or
189+
result = ModelOutput::getATypeNode("pyramid.response.Response~Subclass").getASubclass*()
160190
}
161191

192+
/**
193+
* A source of instances of `pyramid.response.Response`, extend this class to model new instances.
194+
*
195+
* This can include instantiations of the class, return values from function
196+
* calls, or a special parameter that will be set when functions are called by an external
197+
* library.
198+
*
199+
* Use the predicate `Response::instance()` to get references to instances of `pyramid.response.Response`.
200+
*/
162201
abstract class InstanceSource extends DataFlow::LocalSourceNode,
163202
Http::Server::HttpResponse::Range
164203
{ }
165204

166-
/** Gets a reference to an instance of `pyramid.request.Request`. */
205+
/** Gets a reference to an instance of `pyramid.response.Response`. */
167206
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
168207
t.start() and
169208
result instanceof InstanceSource
170209
or
171210
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
172211
}
173212

174-
/** Gets a reference to an instance of `pyramid.request.Request`. */
213+
/** Gets a reference to an instance of `pyramid.response.Response`. */
175214
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
176215

216+
/** An instantiation of the class `pyramid.response.Response` or a subclass. */
177217
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
178-
ClassInstantiation() { this = classRef().getACall() }
218+
ClassInstantiation() { this = subclassRef().getACall() }
179219

180220
override DataFlow::Node getBody() { result = [this.getArg(0), this.getArgByName("body")] }
181221

@@ -186,6 +226,7 @@ module Pyramid {
186226
override string getMimetypeDefault() { result = "text/html" }
187227
}
188228

229+
/** A write to a field that sets the body of a response. */
189230
private class ResponseBodySet extends Http::Server::HttpResponse::Range instanceof DataFlow::AttrWrite
190231
{
191232
string attrName;
@@ -207,6 +248,7 @@ module Pyramid {
207248
}
208249
}
209250

251+
/** A use of the `response` attribute of a `Request`. */
210252
private class RequestResponseAttr extends InstanceSource instanceof DataFlow::AttrRead {
211253
RequestResponseAttr() {
212254
this.getObject() = Request::instance() and this.getAttributeName() = "response"
@@ -219,6 +261,7 @@ module Pyramid {
219261
override string getMimetypeDefault() { result = "text/html" }
220262
}
221263

264+
/** A call to `response.set_cookie`. */
222265
private class SetCookieCall extends Http::Server::CookieWrite::Range, DataFlow::MethodCallNode {
223266
SetCookieCall() { this.calls(instance(), "set_cookie") }
224267

@@ -231,4 +274,33 @@ module Pyramid {
231274
}
232275
}
233276
}
277+
278+
/** Provides models for pyramid http redirects. */
279+
module Redirect {
280+
/** Gets a reference to a subclass of `pyramid.httpexceptions._HTTPMove`, which each each exception class representing an HTTP redirect response is a subclass of. */
281+
API::Node subclassRef() {
282+
result =
283+
API::moduleImport("pyramid")
284+
.getMember("httpexceptions")
285+
.getMember("_HTTPMove")
286+
.getASubclass*() or
287+
result =
288+
ModelOutput::getATypeNode("pyramid.httpexceptions._HTTPMove~Subclass").getASubclass*()
289+
}
290+
291+
/** Gets a call to a pyramid HTTP exception class that represents an HTTP redirect response. */
292+
class PyramidRedirect extends Http::Server::HttpRedirectResponse::Range, DataFlow::CallCfgNode {
293+
PyramidRedirect() { this = subclassRef().getACall() }
294+
295+
override DataFlow::Node getRedirectLocation() {
296+
result = [this.getArg(0), this.getArgByName("location")]
297+
}
298+
299+
override DataFlow::Node getBody() { none() }
300+
301+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
302+
303+
override string getMimetypeDefault() { result = "text/html" }
304+
}
305+
}
234306
}

python/ql/test/library-tests/frameworks/pyramid/pyramid_test.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,8 @@
66
def ignore(*args, **kwargs): pass
77
ensure_tainted = ensure_not_tainted = ignore
88

9-
@view_config(route_name="test1")
10-
def test1(request): # $ requestHandler routedParameter=request
9+
@view_config(route_name="test1") # $ routeSetup
10+
def test1(request): # $ requestHandler
1111
ensure_tainted(
1212
request, # $ tainted
1313

@@ -72,17 +72,17 @@ def test1(request): # $ requestHandler routedParameter=request
7272

7373
return Response("Ok") # $ HttpResponse responseBody="Ok" mimetype=text/html
7474

75-
def test2(request): # $ requestHandler routedParameter=request
75+
def test2(request): # $ requestHandler
7676
ensure_tainted(request) # $ tainted
7777

7878
resp = Response("Ok", content_type="text/plain") # $ HttpResponse responseBody="Ok" mimetype=text/plain
7979
resp.body = "Ok2" # $ HttpResponse responseBody="Ok2" SPURIOUS: mimetype=text/html
8080
return resp
8181

82-
@view_config(route_name="test3", renderer="string")
83-
def test3(context, request): # $ requestHandler routedParameter=request
84-
ensure_tainted(request) # $ tainted
85-
resp = request.response # $ HttpResponse mimetype=text/html
82+
@view_config(route_name="test3", renderer="string") # $ routeSetup
83+
def test3(ctx, req): # $ requestHandler
84+
ensure_tainted(req) # $ tainted
85+
resp = req.response # $ HttpResponse mimetype=text/html
8686
resp.set_cookie("hi", "there") # $ CookieWrite CookieName="hi" CookieValue="there"
8787
resp.set_cookie(value="there", name="hi") # $ CookieWrite CookieName="hi" CookieValue="there"
8888
return "Ok" # $ HttpResponse responseBody="Ok" mimetype=text/html
@@ -91,7 +91,7 @@ def test3(context, request): # $ requestHandler routedParameter=request
9191
with Configurator() as config:
9292
for i in range(1,4):
9393
config.add_route(f"test{i}", f"/test{i}")
94-
config.add_view(test2, route_name="test2")
94+
config.add_view(test2, route_name="test2") # $ routeSetup
9595
config.scan()
9696
server = make_server('127.0.0.1', 8000, config.make_wsgi_app())
9797
print("serving")

0 commit comments

Comments
 (0)