Skip to content

Commit 8d525e5

Browse files
committed
Python: Add support for bottle framework routing and requests.
1 parent 9caa9c1 commit 8d525e5

File tree

10 files changed

+309
-0
lines changed

10 files changed

+309
-0
lines changed

python/ql/src/semmle/python/web/HttpRequest.qll

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,4 @@ import semmle.python.web.flask.Request
33
import semmle.python.web.tornado.Request
44
import semmle.python.web.pyramid.Request
55
import semmle.python.web.twisted.Request
6+
import semmle.python.web.bottle.Request
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import python
2+
import semmle.python.web.Http
3+
import semmle.python.types.Extensions
4+
5+
/** The flask module */
6+
ModuleObject theBottleModule() {
7+
result = ModuleObject::named("bottle")
8+
}
9+
10+
/** The flask app class */
11+
ClassObject theBottleClass() {
12+
result = ModuleObject::named("bottle").getAttribute("Bottle")
13+
}
14+
15+
/** Holds if `route` is routed to `func`
16+
* by decorating `func` with `app.route(route)` or `route(route)`
17+
*/
18+
predicate bottle_route(CallNode route_call, ControlFlowNode route, Function func) {
19+
exists(CallNode decorator_call, string name |
20+
route_call.getFunction().(AttrNode).getObject(name).refersTo(_, theBottleClass(), _) or
21+
route_call.getFunction().refersTo(theBottleModule().getAttribute(name))
22+
|
23+
(name = "route" or name = httpVerbLower()) and
24+
decorator_call.getFunction() = route_call and
25+
route_call.getArg(0) = route and
26+
decorator_call.getArg(0).getNode().(FunctionExpr).getInnerScope() = func
27+
)
28+
}
29+
30+
class BottleRoute extends ControlFlowNode {
31+
32+
BottleRoute() {
33+
bottle_route(this, _, _)
34+
}
35+
36+
string getUrl() {
37+
exists(StrConst url |
38+
bottle_route(this, url.getAFlowNode(), _) and
39+
result = url.getText()
40+
)
41+
}
42+
43+
Function getFunction() {
44+
bottle_route(this, _, result)
45+
}
46+
47+
Parameter getNamedArgument() {
48+
exists(string name, Function func |
49+
func = this.getFunction() and
50+
func.getArgByName(name) = result and
51+
this.getUrl().matches("%<" + name + ">%")
52+
)
53+
}
54+
}
55+
56+
57+
/* bottle module route constants */
58+
59+
class BottleRoutePointToExtension extends CustomPointsToFact {
60+
61+
string name;
62+
63+
BottleRoutePointToExtension() {
64+
exists(DefinitionNode defn |
65+
defn.getScope().(Module).getName() = "bottle" and
66+
this = defn.getValue() and
67+
name = defn.(NameNode).getId()
68+
|
69+
name = "route" or
70+
name = httpVerbLower()
71+
)
72+
}
73+
74+
override predicate pointsTo(Context context, Object value, ClassObject cls, ControlFlowNode origin) {
75+
context.isImport() and
76+
ModuleObject::named("bottle").getAttribute("Bottle").(ClassObject).attributeRefersTo(name, value, cls, origin)
77+
}
78+
}
79+
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import python
2+
3+
4+
import semmle.python.security.TaintTracking
5+
import semmle.python.security.strings.Untrusted
6+
import semmle.python.web.Http
7+
import semmle.python.web.bottle.General
8+
9+
private Object theBottleRequestObject() {
10+
result = theBottleModule().getAttribute("request")
11+
}
12+
13+
class BottleRequestKind extends TaintKind {
14+
15+
BottleRequestKind() {
16+
this = "bottle.request"
17+
}
18+
19+
override TaintKind getTaintOfAttribute(string name) {
20+
result instanceof BottleFormsDict and
21+
(name = "cookies" or name = "query" or name = "form")
22+
or
23+
result instanceof UntrustedStringKind and
24+
(name = "query_string" or name = "url_args")
25+
or
26+
result.(DictKind).getValue() instanceof FileUpload and
27+
name = "files"
28+
}
29+
30+
}
31+
32+
private class RequestSource extends TaintSource {
33+
34+
RequestSource() {
35+
this.(ControlFlowNode).refersTo(theBottleRequestObject())
36+
}
37+
38+
override predicate isSourceOf(TaintKind kind) {
39+
kind instanceof BottleRequestKind
40+
}
41+
42+
}
43+
44+
45+
class BottleFormsDict extends TaintKind {
46+
47+
BottleFormsDict() {
48+
this = "bottle.FormsDict"
49+
}
50+
51+
override TaintKind getTaintForFlowStep(ControlFlowNode fromnode, ControlFlowNode tonode) {
52+
/* Cannot use `getTaintOfAttribute()` as it doesn't bind name */
53+
exists(string name |
54+
tonode = fromnode.(AttrNode).getObject(name) and
55+
result instanceof UntrustedStringKind
56+
|
57+
name != "get" and name != "getunicode" and name != "getall"
58+
)
59+
}
60+
61+
override TaintKind getTaintOfMethodResult(string name) {
62+
(name = "get" or name = "getunicode") and
63+
result instanceof UntrustedStringKind
64+
or
65+
name = "getall" and result.(SequenceKind).getItem() instanceof UntrustedStringKind
66+
}
67+
}
68+
69+
class FileUpload extends TaintKind {
70+
71+
FileUpload() {
72+
this = "bottle.FileUpload"
73+
}
74+
75+
override TaintKind getTaintOfAttribute(string name) {
76+
name = "filename" and result instanceof UntrustedStringKind
77+
or
78+
name = "raw_filename" and result instanceof UntrustedStringKind
79+
or
80+
name = "file" and result instanceof UntrustedFile
81+
}
82+
83+
}
84+
85+
class UntrustedFile extends TaintKind {
86+
87+
UntrustedFile() { this = "Untrusted file" }
88+
89+
}
90+
91+
//
92+
// TO DO.. File uploads -- Should check about file uploads for other frameworks as well.
93+
// Move UntrustedFile to shared location
94+
//
95+
96+
97+
/** Parameter to a bottle request handler function */
98+
class BottleRequestParameter extends TaintSource {
99+
100+
BottleRequestParameter() {
101+
exists(BottleRoute route |
102+
route.getNamedArgument() = this.(ControlFlowNode).getNode()
103+
)
104+
}
105+
106+
override predicate isSourceOf(TaintKind kind) {
107+
kind instanceof UntrustedStringKind
108+
}
109+
110+
override string toString() {
111+
result = "flask.request.args"
112+
}
113+
114+
}
115+
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
| /bye/<name> | test.py:12:1:12:25 | Function bye |
2+
| /hello/<name> | test.py:8:1:8:27 | Function hello |
3+
| /other | test.py:17:1:17:12 | Function other |
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import python
2+
3+
import semmle.python.web.bottle.General
4+
5+
from BottleRoute route
6+
7+
select route.getUrl(), route.getFunction()
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
| ../../../query-tests/Security/lib/bottle.py:64 | LocalRequest() | bottle.request |
2+
| ../../../query-tests/Security/lib/bottle.py:64 | request | bottle.request |
3+
| test.py:3 | ImportMember | bottle.request |
4+
| test.py:3 | request | bottle.request |
5+
| test.py:8 | name | externally controlled string |
6+
| test.py:12 | name | externally controlled string |
7+
| test.py:18 | request | bottle.request |
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
2+
import python
3+
4+
import semmle.python.web.HttpRequest
5+
import semmle.python.security.strings.Untrusted
6+
7+
8+
from TaintSource src, TaintKind kind
9+
where src.isSourceOf(kind) and not kind.matches("tornado%")
10+
select src.getLocation().toString(), src.(ControlFlowNode).getNode().toString(), kind
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
semmle-extractor-options: --max-import-depth=3 --lang=3 -p ../../../query-tests/Security/lib/
2+
optimize: true
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
2+
3+
from bottle import Bottle, route, request
4+
5+
app = Bottle()
6+
7+
@app.route('/hello/<name>')
8+
def hello(name = "World!"):
9+
return "Hello " + name
10+
11+
@route('/bye/<name>')
12+
def bye(name = "World!"):
13+
return "Bye " + name
14+
15+
16+
@route('/other')
17+
def other():
18+
name = request.cookies.username
19+
return "User name is " + name
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
2+
class Bottle(object):
3+
4+
def route(self, path=None, method='GET', **options):
5+
pass
6+
7+
def get(self, path=None, method='GET', **options):
8+
""" Equals :meth:`route`. """
9+
return self.route(path, method, **options)
10+
11+
def post(self, path=None, method='POST', **options):
12+
""" Equals :meth:`route` with a ``POST`` method parameter. """
13+
return self.route(path, method, **options)
14+
15+
def put(self, path=None, method='PUT', **options):
16+
""" Equals :meth:`route` with a ``PUT`` method parameter. """
17+
return self.route(path, method, **options)
18+
19+
def delete(self, path=None, method='DELETE', **options):
20+
""" Equals :meth:`route` with a ``DELETE`` method parameter. """
21+
return self.route(path, method, **options)
22+
23+
def error(self, code=500):
24+
""" Decorator: Register an output handler for a HTTP error code"""
25+
def wrapper(handler):
26+
self.error_handler[int(code)] = handler
27+
return handler
28+
return wrapper
29+
30+
#Use same wrapper logic as the original `bottle` code.
31+
32+
def make_default_app_wrapper(name):
33+
""" Return a callable that relays calls to the current default app. """
34+
35+
@functools.wraps(getattr(Bottle, name))
36+
def wrapper(*a, **ka):
37+
return getattr(app(), name)(*a, **ka)
38+
39+
return wrapper
40+
41+
route = make_default_app_wrapper('route')
42+
get = make_default_app_wrapper('get')
43+
post = make_default_app_wrapper('post')
44+
put = make_default_app_wrapper('put')
45+
delete = make_default_app_wrapper('delete')
46+
patch = make_default_app_wrapper('patch')
47+
error = make_default_app_wrapper('error')
48+
mount = make_default_app_wrapper('mount')
49+
hook = make_default_app_wrapper('hook')
50+
install = make_default_app_wrapper('install')
51+
uninstall = make_default_app_wrapper('uninstall')
52+
url = make_default_app_wrapper('get_url')
53+
54+
class LocalProxy(object):
55+
pass
56+
57+
class LocalRequest(LocalProxy):
58+
pass
59+
60+
class LocalResponse(LocalProxy):
61+
pass
62+
63+
64+
request = LocalRequest()
65+
response = LocalResponse()
66+

0 commit comments

Comments
 (0)