Merge pull request #979 from markshannon/python-falcon

Python: Add support for falcon web API framework.
This commit is contained in:
Taus
2019-02-28 15:47:35 +01:00
committed by GitHub
24 changed files with 375 additions and 16 deletions

View File

@@ -167,6 +167,7 @@ abstract class CollectionKind extends TaintKind {
/* Prevent any collection kinds more than 2 deep */
not this.charAt(2) = "[" and not this.charAt(2) = "{"
}
}
/** A taint kind representing a flat collections of kinds.
@@ -193,7 +194,7 @@ class SequenceKind extends CollectionKind {
tonode.(BinaryExprNode).getAnOperand() = fromnode
)
or
result = this and copy_call(fromnode, tonode)
result = this and TaintFlowImplementation::copyCall(fromnode, tonode)
or
exists(BinaryExprNode mod |
mod = tonode and
@@ -236,20 +237,6 @@ private predicate slice(ControlFlowNode fromnode, SubscriptNode tonode) {
)
}
/* A call that returns a copy (or similar) of the argument */
private predicate copy_call(ControlFlowNode fromnode, CallNode tonode) {
tonode.getFunction().(AttrNode).getObject("copy") = fromnode
or
exists(ModuleObject copy, string name |
name = "copy" or name = "deepcopy" |
copy.attr(name).(FunctionObject).getACall() = tonode and
tonode.getArg(0) = fromnode
)
or
tonode.getFunction().refersTo(Object::builtin("reversed")) and
tonode.getArg(0) = fromnode
}
/** A taint kind representing a mapping of objects to kinds.
* Typically a dict, but can include other mappings.
*/
@@ -272,7 +259,7 @@ class DictKind extends CollectionKind {
result = valueKind and
tonode.(CallNode).getFunction().(AttrNode).getObject("get") = fromnode
or
result = this and copy_call(fromnode, tonode)
result = this and TaintFlowImplementation::copyCall(fromnode, tonode)
or
result = this and
tonode.(CallNode).getFunction().refersTo(theDictType()) and
@@ -1263,6 +1250,20 @@ library module TaintFlowImplementation {
context = fromnode.getContext()
}
/* A call that returns a copy (or similar) of the argument */
predicate copyCall(ControlFlowNode fromnode, CallNode tonode) {
tonode.getFunction().(AttrNode).getObject("copy") = fromnode
or
exists(ModuleObject copy, string name |
name = "copy" or name = "deepcopy" |
copy.attr(name).(FunctionObject).getACall() = tonode and
tonode.getArg(0) = fromnode
)
or
tonode.getFunction().refersTo(Object::builtin("reversed")) and
tonode.getArg(0) = fromnode
}
}
/* Helper predicate for tainted_with */

View File

@@ -96,3 +96,23 @@ private predicate json_load(ControlFlowNode fromnode, CallNode tonode) {
)
}
/** A kind of "taint", representing an open file-like object from an external source. */
class ExternalFileObject extends TaintKind {
ExternalFileObject() {
this = "file[" + any(ExternalStringKind key) + "]"
}
/** Gets the taint kind for the contents of this file */
TaintKind getValue() {
this = "file[" + result + "]"
}
override TaintKind getTaintOfMethodResult(string name) {
name = "read" and result = this.getValue()
}
}

View File

@@ -23,3 +23,37 @@ string httpVerb() {
string httpVerbLower() {
result = httpVerb().toLowerCase()
}
/** Taint kind representing the WSGI environment.
* As specified in PEP 3333. https://www.python.org/dev/peps/pep-3333/#environ-variables
*/
class WsgiEnvironment extends TaintKind {
WsgiEnvironment() { this = "wsgi.environment" }
override TaintKind getTaintForFlowStep(ControlFlowNode fromnode, ControlFlowNode tonode) {
result = this and TaintFlowImplementation::copyCall(fromnode, tonode)
or
result = this and
tonode.(CallNode).getFunction().refersTo(theDictType()) and
tonode.(CallNode).getArg(0) = fromnode
or
exists(StringObject key, string text |
tonode.(CallNode).getFunction().(AttrNode).getObject("get") = fromnode and
tonode.(CallNode).getArg(0).refersTo(key)
or
tonode.(SubscriptNode).getValue() = fromnode and tonode.isLoad() and
tonode.(SubscriptNode).getIndex().refersTo(key)
|
text = key.getText() and result instanceof ExternalStringKind and
(
text = "QUERY_STRING" or
text = "PATH_INFO" or
text.prefix(5) = "HTTP_"
)
)
}
}

View File

@@ -5,3 +5,4 @@ import semmle.python.web.pyramid.Request
import semmle.python.web.twisted.Request
import semmle.python.web.bottle.Request
import semmle.python.web.turbogears.Request
import semmle.python.web.falcon.Request

View File

@@ -5,3 +5,4 @@ import semmle.python.web.tornado.Response
import semmle.python.web.twisted.Response
import semmle.python.web.bottle.Response
import semmle.python.web.turbogears.Response
import semmle.python.web.falcon.Response

View File

@@ -0,0 +1,68 @@
import python
import semmle.python.web.Http
/** The falcon API class */
ClassObject theFalconAPIClass() {
result = ModuleObject::named("falcon").getAttribute("API")
}
/** Holds if `route` is routed to `resource`
*/
private predicate api_route(CallNode route_call, ControlFlowNode route, ClassObject resource) {
route_call.getFunction().(AttrNode).getObject("add_route").refersTo(_, theFalconAPIClass(), _) and
route_call.getArg(0) = route and
route_call.getArg(1).refersTo(_, resource, _)
}
private predicate route(FalconRoute route, Function target, string funcname) {
route.getResourceClass().lookupAttribute("on_" + funcname).(FunctionObject).getFunction() = target
}
class FalconRoute extends ControlFlowNode {
FalconRoute() {
api_route(this, _, _)
}
string getUrl() {
exists(StrConst url |
api_route(this, url.getAFlowNode(), _) and
result = url.getText()
)
}
ClassObject getResourceClass() {
api_route(this, _, result)
}
FalconHandlerFunction getHandlerFunction(string method) {
route(this, result, method)
}
}
class FalconHandlerFunction extends Function {
FalconHandlerFunction() {
route(_, this, _)
}
private string methodName() {
route(_, this, result)
}
string getMethod() {
result = this.methodName().toUpperCase()
}
Parameter getRequest() {
result = this.getArg(1)
}
Parameter getResponse() {
result = this.getArg(2)
}
}

View File

@@ -0,0 +1,56 @@
import python
import semmle.python.security.TaintTracking
import semmle.python.web.Http
import semmle.python.web.falcon.General
import semmle.python.security.strings.External
/** https://falcon.readthedocs.io/en/stable/api/request_and_response.html */
class FalconRequest extends TaintKind {
FalconRequest() {
this = "falcon.request"
}
override TaintKind getTaintOfAttribute(string name) {
name = "env" and result instanceof WsgiEnvironment
or
result instanceof ExternalStringKind and
(
name = "uri" or name = "url" or
name = "forwarded_uri" or
name = "relative_uri" or
name = "query_string"
)
or
result instanceof ExternalStringDictKind and
(
name = "cookies" or name = "params"
)
or
name = "stream" and result instanceof ExternalFileObject
}
override TaintKind getTaintOfMethodResult(string name) {
name = "get_param" and result instanceof ExternalStringKind
or
name = "get_param_as_json" and result instanceof ExternalJsonKind
or
name = "get_param_as_list" and result instanceof ExternalStringSequenceKind
}
}
class FalconRequestParameter extends TaintSource {
FalconRequestParameter() {
exists(FalconHandlerFunction f |
f.getRequest() = this.(ControlFlowNode).getNode()
)
}
override predicate isSourceOf(TaintKind k) {
k instanceof FalconRequest
}
}

View File

@@ -0,0 +1,48 @@
import python
import semmle.python.security.TaintTracking
import semmle.python.web.Http
import semmle.python.web.falcon.General
import semmle.python.security.strings.External
/** https://falcon.readthedocs.io/en/stable/api/request_and_response.html */
class FalconResponse extends TaintKind {
FalconResponse() {
this = "falcon.response"
}
}
class FalconResponseParameter extends TaintSource {
FalconResponseParameter() {
exists(FalconHandlerFunction f |
f.getResponse() = this.(ControlFlowNode).getNode()
)
}
override predicate isSourceOf(TaintKind k) {
k instanceof FalconResponse
}
}
class FalconResponseBodySink extends TaintSink {
FalconResponseBodySink() {
exists(AttrNode attr |
any(FalconResponse f).taints(attr.getObject("body")) |
attr.(DefinitionNode).getValue() = this
)
}
override predicate sinks(TaintKind kind) {
kind instanceof StringKind
}
}

View File

@@ -0,0 +1,3 @@
| /hello | delete | test.py:22:5:22:35 | Function on_delete |
| /hello | get | test.py:9:5:9:32 | Function on_get |
| /hello | post | test.py:19:5:19:33 | Function on_post |

View File

@@ -0,0 +1,8 @@
import python
import semmle.python.web.falcon.General
from FalconRoute route, string method
select route.getUrl(), method, route.getHandlerFunction(method)

View File

@@ -0,0 +1 @@
| test.py:17 | Attribute() | externally controlled string |

View File

@@ -0,0 +1,10 @@
import python
import semmle.python.web.HttpRequest
import semmle.python.web.HttpResponse
import semmle.python.security.strings.Untrusted
from TaintSink sink, TaintKind kind
where sink.sinks(kind)
select sink.getLocation().toString(), sink.(ControlFlowNode).getNode().toString(), kind

View File

@@ -0,0 +1,3 @@
| test.py:9 | req | falcon.request |
| test.py:19 | req | falcon.request |
| test.py:22 | req | falcon.request |

View File

@@ -0,0 +1,10 @@
import python
import semmle.python.web.HttpRequest
import semmle.python.security.strings.Untrusted
from TaintSource src, TaintKind kind
where src.isSourceOf(kind) and not kind.matches("tornado%")
select src.getLocation().toString(), src.(ControlFlowNode).getNode().toString(), kind

View File

@@ -0,0 +1,25 @@
| test.py:9 | req | falcon.request |
| test.py:9 | resp | falcon.response |
| test.py:10 | Attribute | file[externally controlled string] |
| test.py:10 | Attribute() | externally controlled string |
| test.py:10 | req | falcon.request |
| test.py:11 | Attribute() | externally controlled string |
| test.py:11 | Attribute() | json[externally controlled string] |
| test.py:11 | raw_json | externally controlled string |
| test.py:12 | resp | falcon.response |
| test.py:13 | Dict | {externally controlled string} |
| test.py:13 | Dict | {json[externally controlled string]} |
| test.py:15 | result | externally controlled string |
| test.py:15 | result | json[externally controlled string] |
| test.py:17 | resp | falcon.response |
| test.py:17 | result | {externally controlled string} |
| test.py:17 | result | {json[externally controlled string]} |
| test.py:19 | req | falcon.request |
| test.py:19 | resp | falcon.response |
| test.py:22 | req | falcon.request |
| test.py:22 | resp | falcon.response |
| test.py:23 | Attribute | wsgi.environment |
| test.py:23 | req | falcon.request |
| test.py:24 | Subscript | externally controlled string |
| test.py:24 | env | wsgi.environment |
| test.py:25 | qs | externally controlled string |

View File

@@ -0,0 +1,13 @@
import python
import semmle.python.web.HttpRequest
import semmle.python.web.HttpResponse
import semmle.python.security.strings.Untrusted
from TaintedNode node
where node.getLocation().getFile().getName().matches("%falcon/test.py")
select node.getLocation().toString(), node.getNode().getNode().toString(), node.getTaintKind()

View File

@@ -0,0 +1,2 @@
semmle-extractor-options: --max-import-depth=3 --lang=3 -p ../../../query-tests/Security/lib/
optimize: true

View File

@@ -0,0 +1,28 @@
import json
from falcon import API
app = API()
class Handler(object):
def on_get(self, req, resp):
raw_json = req.stream.read()
result = json.loads(raw_json)
resp.status = 200
result = {
'status': 'success',
'data': result
}
resp.body = json.dumps(result)
def on_post(self, req, resp):
pass
def on_delete(self, req, resp):
env = req.env
qs = env["QUERY_STRING"]
return qs
app.add_route('/hello', Handler())

View File

@@ -0,0 +1,4 @@
from falcon.api import API
from falcon.request import Request
from falcon.response import Response

View File

@@ -0,0 +1,14 @@
"""Falcon API class."""
class API(object):
def add_route(self, uri_template, resource, **kwargs):
pass
def add_sink(self, sink, prefix=r'/'):
pass
def add_error_handler(self, exception, handler=None):
pass

View File

@@ -0,0 +1,3 @@
class Request(object):
pass

View File

@@ -0,0 +1,4 @@
class Response(object):
pass