Skip to content

Commit 22287be

Browse files
authored
Merge pull request github#17370 from Kwstubbs/Bottle/Tornado-HeaderSupport
Python: Bottle Framework Support
2 parents 7a06574 + ac411f1 commit 22287be

File tree

10 files changed

+233
-0
lines changed

10 files changed

+233
-0
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
---
2+
category: majorAnalysis
3+
---
4+
* Added modeling of the `bottle` framework, leading to new remote flow sources and header writes

python/ql/lib/semmle/python/Frameworks.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ private import semmle.python.frameworks.Anyio
1515
private import semmle.python.frameworks.Asyncpg
1616
private import semmle.python.frameworks.Baize
1717
private import semmle.python.frameworks.BSon
18+
private import semmle.python.frameworks.Bottle
1819
private import semmle.python.frameworks.CassandraDriver
1920
private import semmle.python.frameworks.Cherrypy
2021
private import semmle.python.frameworks.ClickhouseDriver
Lines changed: 175 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,175 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `bottle` PyPI package.
3+
* See https://bottlepy.org/docs/dev/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.Concepts
8+
private import semmle.python.ApiGraphs
9+
private import semmle.python.dataflow.new.RemoteFlowSources
10+
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
11+
private import semmle.python.frameworks.Stdlib
12+
13+
/**
14+
* INTERNAL: Do not use.
15+
*
16+
* Provides models for the `bottle` PyPI package.
17+
* See https://bottlepy.org/docs/dev/.
18+
*/
19+
module Bottle {
20+
/** Gets a reference to the `bottle` module. */
21+
API::Node bottle() { result = API::moduleImport("bottle") }
22+
23+
/** Provides models for the `bottle` module. */
24+
module BottleModule {
25+
/**
26+
* Provides models for Bottle applications.
27+
*/
28+
module App {
29+
/** Gets a reference to a Bottle application (an instance of `bottle.Bottle`) */
30+
API::Node app() { result = bottle().getMember(["Bottle", "app"]).getReturn() }
31+
}
32+
33+
/** Provides models for functions that are possible "views" */
34+
module View {
35+
/**
36+
* A Bottle view callable, that handles incoming requests.
37+
*/
38+
class ViewCallable extends Function {
39+
ViewCallable() { this = any(BottleRouteSetup rs).getARequestHandler() }
40+
}
41+
42+
/** Get methods that reprsent a route in Bottle */
43+
string routeMethods() { result = ["route", "get", "post", "put", "delete", "patch"] }
44+
45+
private class BottleRouteSetup extends Http::Server::RouteSetup::Range, DataFlow::CallCfgNode {
46+
BottleRouteSetup() {
47+
this =
48+
[
49+
App::app().getMember(routeMethods()).getACall(),
50+
bottle().getMember(routeMethods()).getACall()
51+
]
52+
}
53+
54+
override DataFlow::Node getUrlPatternArg() {
55+
result in [this.getArg(0), this.getArgByName("route")]
56+
}
57+
58+
override string getFramework() { result = "Bottle" }
59+
60+
override Parameter getARoutedParameter() { none() }
61+
62+
override Function getARequestHandler() { result.getADecorator().getAFlowNode() = node }
63+
}
64+
}
65+
66+
/** Provides models for the `bottle.response` module */
67+
module Response {
68+
/** Gets a reference to the `bottle.response` module or instantiation of Bottle Response class. */
69+
API::Node response() {
70+
result = [bottle().getMember("response"), bottle().getMember("Response").getReturn()]
71+
}
72+
73+
/** A response returned by a view callable. */
74+
class BottleReturnResponse extends Http::Server::HttpResponse::Range {
75+
BottleReturnResponse() {
76+
this.asCfgNode() = any(View::ViewCallable vc).getAReturnValueFlowNode()
77+
}
78+
79+
override DataFlow::Node getBody() { result = this }
80+
81+
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
82+
83+
override string getMimetypeDefault() { result = "text/html" }
84+
}
85+
86+
/**
87+
* A call to the `bottle.BaseResponse.set_header` or `bottle.BaseResponse.add_header` method.
88+
*
89+
* See https://bottlepy.org/docs/dev/api.html#bottle.BaseResponse.set_header
90+
*/
91+
class BottleResponseHandlerSetHeaderCall extends Http::Server::ResponseHeaderWrite::Range,
92+
DataFlow::MethodCallNode
93+
{
94+
BottleResponseHandlerSetHeaderCall() {
95+
this = response().getMember(["set_header", "add_header"]).getACall()
96+
}
97+
98+
override DataFlow::Node getNameArg() {
99+
result in [this.getArg(0), this.getArgByName("name")]
100+
}
101+
102+
override DataFlow::Node getValueArg() {
103+
result in [this.getArg(1), this.getArgByName("value")]
104+
}
105+
106+
override predicate nameAllowsNewline() { none() }
107+
108+
override predicate valueAllowsNewline() { none() }
109+
}
110+
}
111+
112+
/** Provides models for the `bottle.request` module */
113+
module Request {
114+
/** Gets a reference to the `bottle.request` module. */
115+
API::Node request() { result = bottle().getMember("request") }
116+
117+
private class Request extends RemoteFlowSource::Range {
118+
Request() { this = request().asSource() }
119+
120+
override string getSourceType() { result = "bottle.request" }
121+
}
122+
123+
/**
124+
* Taint propagation for `bottle.request`.
125+
*
126+
* See https://bottlepy.org/docs/dev/api.html#bottle.request
127+
*/
128+
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
129+
InstanceTaintSteps() { this = "bottle.request" }
130+
131+
override DataFlow::Node getInstance() { result = request().getAValueReachableFromSource() }
132+
133+
override string getAttributeName() {
134+
result in [
135+
"headers", "query", "forms", "params", "json", "url", "body", "fullpath",
136+
"query_string"
137+
]
138+
}
139+
140+
override string getMethodName() { none() }
141+
142+
override string getAsyncMethodName() { none() }
143+
}
144+
}
145+
146+
/** Provides models for the `bottle.headers` module */
147+
module Headers {
148+
/** Gets a reference to the `bottle.headers` module. */
149+
API::Node headers() { result = bottle().getMember("response").getMember("headers") }
150+
151+
/** A dict-like write to a response header. */
152+
class HeaderWriteSubscript extends Http::Server::ResponseHeaderWrite::Range, DataFlow::Node {
153+
DataFlow::Node name;
154+
DataFlow::Node value;
155+
156+
HeaderWriteSubscript() {
157+
exists(SubscriptNode subscript |
158+
this.asCfgNode() = subscript and
159+
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
160+
name.asCfgNode() = subscript.getIndex() and
161+
subscript.getObject() = headers().asSource().asCfgNode()
162+
)
163+
}
164+
165+
override DataFlow::Node getNameArg() { result = name }
166+
167+
override DataFlow::Node getValueArg() { result = value }
168+
169+
override predicate nameAllowsNewline() { none() }
170+
171+
override predicate valueAllowsNewline() { none() }
172+
}
173+
}
174+
}
175+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
testFailures
2+
failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import python
2+
import experimental.meta.ConceptsTest
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
argumentToEnsureNotTaintedNotMarkedAsSpurious
2+
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
3+
testFailures
4+
failures
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
import experimental.meta.InlineTaintTest
2+
import MakeInlineTaintTest<TestTaintTrackingConfig>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# Source: https://bottlepy.org/docs/dev/tutorial.html#the-application-object
2+
from bottle import Bottle, run
3+
4+
app = Bottle()
5+
6+
@app.route('/hello') # $ routeSetup="/hello"
7+
def hello(): # $ requestHandler
8+
return "Hello World!" # $ HttpResponse responseBody="Hello World!" mimetype=text/html
9+
10+
if __name__ == '__main__':
11+
app.run(host='localhost', port=8080)
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import bottle
2+
from bottle import Bottle, response, request
3+
4+
app = Bottle()
5+
@app.route('/test', method=['OPTIONS', 'GET']) # $ routeSetup="/test"
6+
def test1(): # $ requestHandler
7+
response.headers['Content-type'] = 'application/json' # $ headerWriteName='Content-type' headerWriteValue='application/json'
8+
response.set_header('Content-type', 'application/json') # $ headerWriteName='Content-type' headerWriteValue='application/json'
9+
return '[1]' # $ HttpResponse responseBody='[1]' mimetype=text/html
10+
11+
app.run()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import bottle
2+
from bottle import response, request
3+
4+
5+
app = bottle.app()
6+
@app.route('/test', method=['OPTIONS', 'GET']) # $ routeSetup="/test"
7+
def test1(): # $ requestHandler
8+
9+
ensure_tainted(
10+
request.headers, # $ tainted
11+
request.headers, # $ tainted
12+
request.forms, # $ tainted
13+
request.params, # $ tainted
14+
request.url, # $ tainted
15+
request.body, # $ tainted
16+
request.fullpath, # $ tainted
17+
request.query_string # $ tainted
18+
)
19+
return '[1]' # $ HttpResponse mimetype=text/html responseBody='[1]'
20+
21+
app.run()

0 commit comments

Comments
 (0)