mirror of
https://github.com/github/codeql.git
synced 2025-12-16 16:53:25 +01:00
Merge pull request #16300 from joefarebrother/python-pyramid
Python: Model the Pyramid framework
This commit is contained in:
4
python/ql/lib/change-notes/2024-04-30-pyramid.md
Normal file
4
python/ql/lib/change-notes/2024-04-30-pyramid.md
Normal file
@@ -0,0 +1,4 @@
|
||||
---
|
||||
category: majorAnalysis
|
||||
---
|
||||
* Added modeling of the `pyramid` framework, leading to new remote flow sources and sinks.
|
||||
@@ -56,6 +56,7 @@ private import semmle.python.frameworks.PyMongo
|
||||
private import semmle.python.frameworks.Pymssql
|
||||
private import semmle.python.frameworks.PyMySQL
|
||||
private import semmle.python.frameworks.Pyodbc
|
||||
private import semmle.python.frameworks.Pyramid
|
||||
private import semmle.python.frameworks.Requests
|
||||
private import semmle.python.frameworks.RestFramework
|
||||
private import semmle.python.frameworks.Rsa
|
||||
|
||||
299
python/ql/lib/semmle/python/frameworks/Pyramid.qll
Normal file
299
python/ql/lib/semmle/python/frameworks/Pyramid.qll
Normal file
@@ -0,0 +1,299 @@
|
||||
/**
|
||||
* Provides classes modeling security-relevant aspects of the `pyramid` PyPI package.
|
||||
* See https://docs.pylonsproject.org/projects/pyramid/.
|
||||
*/
|
||||
|
||||
private import python
|
||||
private import semmle.python.dataflow.new.RemoteFlowSources
|
||||
private import semmle.python.dataflow.new.TaintTracking
|
||||
private import semmle.python.Concepts
|
||||
private import semmle.python.ApiGraphs
|
||||
private import semmle.python.dataflow.new.FlowSummary
|
||||
private import semmle.python.frameworks.internal.PoorMansFunctionResolution
|
||||
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
|
||||
private import semmle.python.frameworks.data.ModelsAsData
|
||||
private import semmle.python.frameworks.Stdlib
|
||||
|
||||
/**
|
||||
* Provides models for the `pyramid` PyPI package.
|
||||
* See https://docs.pylonsproject.org/projects/pyramid/.
|
||||
*/
|
||||
module Pyramid {
|
||||
/** Provides models for pyramid View callables. */
|
||||
module View {
|
||||
/** A dataflow node that sets up a route on a server using the Pyramid framework. */
|
||||
abstract private class PyramidRouteSetup extends Http::Server::RouteSetup::Range {
|
||||
override string getFramework() { result = "Pyramid" }
|
||||
}
|
||||
|
||||
/**
|
||||
* A Pyramid view callable, that handles incoming requests.
|
||||
*/
|
||||
class ViewCallable extends Function {
|
||||
ViewCallable() { this = any(PyramidRouteSetup rs).getARequestHandler() }
|
||||
|
||||
/** Gets the `request` parameter of this callable. */
|
||||
Parameter getRequestParameter() {
|
||||
this.getPositionalParameterCount() = 1 and
|
||||
result = this.getArg(0)
|
||||
or
|
||||
this.getPositionalParameterCount() = 2 and
|
||||
result = this.getArg(1)
|
||||
}
|
||||
}
|
||||
|
||||
/** A pyramid route setup using the `pyramid.view.view_config` decorator. */
|
||||
private class DecoratorSetup extends PyramidRouteSetup {
|
||||
DecoratorSetup() {
|
||||
this = API::moduleImport("pyramid").getMember("view").getMember("view_config").getACall()
|
||||
}
|
||||
|
||||
override Function getARequestHandler() { result.getADecorator() = this.asExpr() }
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() { none() } // there is a `route_name` arg, but that does not contain the url pattern
|
||||
|
||||
override Parameter getARoutedParameter() { none() }
|
||||
}
|
||||
|
||||
/** A pyramid route setup using a call to `pyramid.config.Configurator.add_view`. */
|
||||
private class ConfiguratorSetup extends PyramidRouteSetup instanceof Configurator::AddViewCall {
|
||||
override Function getARequestHandler() {
|
||||
this.(Configurator::AddViewCall).getViewArg() = poorMansFunctionTracker(result)
|
||||
}
|
||||
|
||||
override DataFlow::Node getUrlPatternArg() { none() } // there is a `route_name` arg, but that does not contain the url pattern
|
||||
|
||||
override Parameter getARoutedParameter() { none() }
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides models for `pyramid.config.Configurator` */
|
||||
module Configurator {
|
||||
/** Gets a reference to the class `pyramid.config.Configurator`. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("pyramid").getMember("config").getMember("Configurator")
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.config.Configurator`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result = classRef().getACall()
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.config.Configurator`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** A call to the `add_view` method of an instance of `pyramid.config.Configurator`. */
|
||||
class AddViewCall extends DataFlow::MethodCallNode {
|
||||
AddViewCall() { this.calls(instance(), "add_view") }
|
||||
|
||||
/** Gets the `view` argument of this call. */
|
||||
DataFlow::Node getViewArg() { result = [this.getArg(0), this.getArgByName("view")] }
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides modeling for pyramid requests. */
|
||||
module Request {
|
||||
/**
|
||||
* A source of instances of `pyramid.request.Request`, extend this class to model new instances.
|
||||
*
|
||||
* Use the predicate `Request::instance()` to get references to instances of `pyramid.request.Request`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode { }
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.request.Request`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.request.Request`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
private class RequestParameter extends InstanceSource, RemoteFlowSource::Range instanceof DataFlow::ParameterNode
|
||||
{
|
||||
RequestParameter() { this.getParameter() = any(View::ViewCallable vc).getRequestParameter() }
|
||||
|
||||
override string getSourceType() { result = "Pyramid request parameter" }
|
||||
}
|
||||
|
||||
/** Taint steps for request instances. */
|
||||
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
|
||||
InstanceTaintSteps() { this = "pyramid.request.Request" }
|
||||
|
||||
override DataFlow::Node getInstance() { result = instance() }
|
||||
|
||||
override string getAttributeName() {
|
||||
result in [
|
||||
"accept", "accept_charset", "accept_encoding", "accept_language", "application_url",
|
||||
"as_bytes", "authorization", "body", "body_file", "body_file_raw", "body_file_seekable",
|
||||
"cache_control", "client_addr", "content_type", "cookies", "domain", "headers", "host",
|
||||
"host_port", "host_url", "GET", "if_match", "if_none_match", "if_range",
|
||||
"if_none_match", "json", "json_body", "matchdict", "params", "path", "path_info",
|
||||
"path_qs", "path_url", "POST", "pragma", "query_string", "range", "referer", "referrer",
|
||||
"text", "url", "urlargs", "urlvars", "user_agent"
|
||||
]
|
||||
}
|
||||
|
||||
override string getMethodName() {
|
||||
result in ["as_bytes", "copy", "copy_get", "path_info_peek", "path_info_pop"]
|
||||
}
|
||||
|
||||
override string getAsyncMethodName() { none() }
|
||||
}
|
||||
|
||||
/** A call to a method of a `request` that copies the request. */
|
||||
private class RequestCopyCall extends InstanceSource, DataFlow::MethodCallNode {
|
||||
RequestCopyCall() { this.calls(instance(), ["copy", "copy_get"]) }
|
||||
}
|
||||
|
||||
/** A member of a request that is a file-like object. */
|
||||
private class RequestBodyFileLike extends Stdlib::FileLikeObject::InstanceSource instanceof DataFlow::AttrRead
|
||||
{
|
||||
RequestBodyFileLike() {
|
||||
this.getObject() = instance() and
|
||||
this.getAttributeName() = ["body_file", "body_file_raw", "body_file_seekable"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides modeling for pyramid responses. */
|
||||
module Response {
|
||||
/** A response returned by a view callable. */
|
||||
private class PyramidReturnResponse extends Http::Server::HttpResponse::Range {
|
||||
PyramidReturnResponse() {
|
||||
this.asCfgNode() = any(View::ViewCallable vc).getAReturnValueFlowNode() and
|
||||
not this = instance()
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { result = this }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** Gets a reference to the class `pyramid.response.Response`. */
|
||||
API::Node classRef() {
|
||||
result = API::moduleImport("pyramid").getMember("response").getMember("Response")
|
||||
}
|
||||
|
||||
/**
|
||||
* A source of instances of `pyramid.response.Response`, extend this class to model new instances.
|
||||
*
|
||||
* This can include instantiations of the class, return values from function
|
||||
* calls, or a special parameter that will be set when functions are called by an external
|
||||
* library.
|
||||
*
|
||||
* Use the predicate `Response::instance()` to get references to instances of `pyramid.response.Response`.
|
||||
*/
|
||||
abstract class InstanceSource extends DataFlow::LocalSourceNode,
|
||||
Http::Server::HttpResponse::Range
|
||||
{ }
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.response.Response`. */
|
||||
private DataFlow::TypeTrackingNode instance(DataFlow::TypeTracker t) {
|
||||
t.start() and
|
||||
result instanceof InstanceSource
|
||||
or
|
||||
exists(DataFlow::TypeTracker t2 | result = instance(t2).track(t2, t))
|
||||
}
|
||||
|
||||
/** Gets a reference to an instance of `pyramid.response.Response`. */
|
||||
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
|
||||
|
||||
/** An instantiation of the class `pyramid.response.Response` or a subclass. */
|
||||
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
|
||||
ClassInstantiation() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getBody() { result = [this.getArg(0), this.getArgByName("body")] }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() {
|
||||
result = [this.getArg(4), this.getArgByName("content_type")]
|
||||
}
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** A write to a field that sets the body of a response. */
|
||||
private class ResponseBodySet extends Http::Server::HttpResponse::Range instanceof DataFlow::AttrWrite
|
||||
{
|
||||
string attrName;
|
||||
|
||||
ResponseBodySet() {
|
||||
this.getObject() = instance() and
|
||||
this.getAttributeName() = attrName and
|
||||
attrName in ["body", "body_file", "json", "json_body", "text", "ubody", "unicode_body"]
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { result = this.(DataFlow::AttrWrite).getValue() }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() {
|
||||
if attrName in ["json", "json_body"]
|
||||
then result = "application/json"
|
||||
else result = "text/html"
|
||||
}
|
||||
}
|
||||
|
||||
/** A use of the `response` attribute of a `Request`. */
|
||||
private class RequestResponseAttr extends InstanceSource instanceof DataFlow::AttrRead {
|
||||
RequestResponseAttr() {
|
||||
this.getObject() = Request::instance() and this.getAttributeName() = "response"
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
|
||||
/** A call to `response.set_cookie`. */
|
||||
private class SetCookieCall extends Http::Server::CookieWrite::Range, DataFlow::MethodCallNode {
|
||||
SetCookieCall() { this.calls(instance(), "set_cookie") }
|
||||
|
||||
override DataFlow::Node getHeaderArg() { none() }
|
||||
|
||||
override DataFlow::Node getNameArg() { result = [this.getArg(0), this.getArgByName("name")] }
|
||||
|
||||
override DataFlow::Node getValueArg() {
|
||||
result = [this.getArg(1), this.getArgByName("value")]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Provides models for pyramid http redirects. */
|
||||
module Redirect {
|
||||
/** Gets a reference to a class that represents an HTTP redirect response.. */
|
||||
API::Node classRef() {
|
||||
result =
|
||||
API::moduleImport("pyramid")
|
||||
.getMember("httpexceptions")
|
||||
.getMember([
|
||||
"HTTPMultipleChoices", "HTTPMovedPermanently", "HTTPFound", "HTTPSeeOther",
|
||||
"HTTPUseProxy", "HTTPTemporaryRedirect", "HTTPPermanentRedirect"
|
||||
])
|
||||
}
|
||||
|
||||
/** A call to a pyramid HTTP exception class that represents an HTTP redirect response. */
|
||||
class PyramidRedirect extends Http::Server::HttpRedirectResponse::Range, DataFlow::CallCfgNode {
|
||||
PyramidRedirect() { this = classRef().getACall() }
|
||||
|
||||
override DataFlow::Node getRedirectLocation() {
|
||||
result = [this.getArg(0), this.getArgByName("location")]
|
||||
}
|
||||
|
||||
override DataFlow::Node getBody() { none() }
|
||||
|
||||
override DataFlow::Node getMimetypeOrContentTypeArg() { none() }
|
||||
|
||||
override string getMimetypeDefault() { result = "text/html" }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import python
|
||||
import experimental.meta.ConceptsTest
|
||||
@@ -0,0 +1,4 @@
|
||||
argumentToEnsureNotTaintedNotMarkedAsSpurious
|
||||
untaintedArgumentToEnsureTaintedNotMarkedAsMissing
|
||||
testFailures
|
||||
failures
|
||||
@@ -0,0 +1,2 @@
|
||||
import experimental.meta.InlineTaintTest
|
||||
import MakeInlineTaintTest<TestTaintTrackingConfig>
|
||||
153
python/ql/test/library-tests/frameworks/pyramid/pyramid_test.py
Normal file
153
python/ql/test/library-tests/frameworks/pyramid/pyramid_test.py
Normal file
@@ -0,0 +1,153 @@
|
||||
from pyramid.view import view_config
|
||||
from pyramid.config import Configurator
|
||||
from pyramid.response import Response
|
||||
from pyramid.httpexceptions import HTTPMultipleChoices, HTTPMovedPermanently, HTTPFound, HTTPSeeOther, HTTPUseProxy, HTTPTemporaryRedirect, HTTPPermanentRedirect
|
||||
from wsgiref.simple_server import make_server
|
||||
|
||||
def ignore(*args, **kwargs): pass
|
||||
ensure_tainted = ensure_not_tainted = ignore
|
||||
|
||||
@view_config(route_name="test1") # $ routeSetup
|
||||
def test1(request): # $ requestHandler
|
||||
ensure_tainted(
|
||||
request, # $ tainted
|
||||
|
||||
request.accept, # $ tainted
|
||||
request.accept_charset, # $ tainted
|
||||
request.accept_encoding, # $ tainted
|
||||
request.accept_language, # $ tainted
|
||||
request.authorization, # $ tainted
|
||||
request.cache_control, # $ tainted
|
||||
request.client_addr, # $ tainted
|
||||
request.content_type, # $ tainted
|
||||
request.domain, # $ tainted
|
||||
request.host, # $ tainted
|
||||
request.host_port, # $ tainted
|
||||
request.host_url, # $ tainted
|
||||
request.if_match, # $ tainted
|
||||
request.if_none_match, # $ tainted
|
||||
request.if_range, # $ tainted
|
||||
request.pragma, # $ tainted
|
||||
request.range, # $ tainted
|
||||
request.referer, # $ tainted
|
||||
request.referrer, # $ tainted
|
||||
request.user_agent, # $ tainted
|
||||
|
||||
request.as_bytes, # $ tainted
|
||||
|
||||
request.body, # $ tainted
|
||||
request.body_file.read(), # $ tainted
|
||||
request.body_file_raw.read(), # $ tainted
|
||||
request.body_file_seekable.read(),# $ tainted
|
||||
|
||||
request.json, # $ tainted
|
||||
request.json_body, # $ tainted
|
||||
request.json['a']['b'][0]['c'], # $ tainted
|
||||
|
||||
request.text, # $ tainted
|
||||
|
||||
request.matchdict, # $ tainted
|
||||
|
||||
request.path, # $ tainted
|
||||
request.path_info, # $ tainted
|
||||
request.path_info_peek(), # $ tainted
|
||||
request.path_info_pop(), # $ tainted
|
||||
request.path_qs, # $ tainted
|
||||
request.path_url, # $ tainted
|
||||
request.query_string, # $ tainted
|
||||
|
||||
request.url, # $ tainted
|
||||
request.urlargs, # $ tainted
|
||||
request.urlvars, # $ tainted
|
||||
|
||||
request.GET['a'], # $ tainted
|
||||
request.POST['b'], # $ tainted
|
||||
request.cookies['c'], # $ tainted
|
||||
request.params['d'], # $ tainted
|
||||
request.headers['X-My-Header'], # $ tainted
|
||||
request.GET.values(), # $ tainted
|
||||
|
||||
request.copy(), # $ tainted
|
||||
request.copy_get(), # $ tainted
|
||||
request.copy().GET['a'], # $ tainted
|
||||
request.copy_get().body # $ tainted
|
||||
)
|
||||
|
||||
return Response("Ok") # $ HttpResponse responseBody="Ok" mimetype=text/html
|
||||
|
||||
def test2(request): # $ requestHandler
|
||||
ensure_tainted(request) # $ tainted
|
||||
|
||||
resp = Response("Ok", content_type="text/plain") # $ HttpResponse responseBody="Ok" mimetype=text/plain
|
||||
resp.body = "Ok2" # $ HttpResponse responseBody="Ok2" SPURIOUS: mimetype=text/html
|
||||
return resp
|
||||
|
||||
@view_config(route_name="test3", renderer="string") # $ routeSetup
|
||||
def test3(ctx, req): # $ requestHandler
|
||||
ensure_tainted(req) # $ tainted
|
||||
resp = req.response # $ HttpResponse mimetype=text/html
|
||||
resp.set_cookie("hi", "there") # $ CookieWrite CookieName="hi" CookieValue="there"
|
||||
resp.set_cookie(value="there", name="hi") # $ CookieWrite CookieName="hi" CookieValue="there"
|
||||
return "Ok" # $ HttpResponse responseBody="Ok" mimetype=text/html
|
||||
|
||||
@view_config(route_name="test4", renderer="string") # $ routeSetup
|
||||
def test4(request): # $ requestHandler
|
||||
a = HTTPMultipleChoices("redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
b = HTTPMovedPermanently(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
c = HTTPFound(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
d = HTTPSeeOther(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
e = HTTPUseProxy(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
f = HTTPTemporaryRedirect(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
g = HTTPPermanentRedirect(location="redirect") # $HttpResponse mimetype=text/html HttpRedirectResponse redirectLocation="redirect"
|
||||
raise a
|
||||
|
||||
# Unsupported cases
|
||||
class Test5:
|
||||
def __init__(self, request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
self.req = request
|
||||
|
||||
@view_config(route_name="test5", renderer="string") # $ routeSetup
|
||||
def test5(self): # $ requestHandler
|
||||
ensure_not_tainted(self) # $ SPURIOUS: tainted
|
||||
ensure_tainted(self.req) # $ MISSING: tainted
|
||||
return "Ok" # $ HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
@view_config(route_name="test6", attr="test6method", renderer="string") # $ routeSetup
|
||||
class Test6:
|
||||
def __init__(self, request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
self.req = request
|
||||
|
||||
def test6method(self): # $ MISSING: requestHandler
|
||||
ensure_not_tainted(self)
|
||||
ensure_tainted(self.req) # $ MISSING: tainted
|
||||
return "Ok" # $ MISSING: HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
@view_config(route_name="test6", renderer="string") # $ routeSetup
|
||||
class Test6:
|
||||
def __init__(self, context, request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
self.req = request
|
||||
|
||||
def __call__(self): # $ MISSING: requestHandler
|
||||
ensure_not_tainted(self)
|
||||
ensure_tainted(self.req) # $ MISSING: tainted
|
||||
return "Ok" # $ MISSING: HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
class Test7:
|
||||
def __call__(self,context,request): # $ MISSING: requestHandler
|
||||
ensure_tainted(request) # $ MISSING: tainted
|
||||
return "Ok" # $ MISSING: HttpResponse mimetype=text/html responseBody="Ok"
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
with Configurator() as config:
|
||||
for i in range(1,8):
|
||||
config.add_route(f"test{i}", f"/test{i}")
|
||||
config.add_view(test2, route_name="test2") # $ routeSetup
|
||||
config.add_view(Test7(), route_name="test7", renderer="string") # $ routeSetup
|
||||
config.scan()
|
||||
server = make_server('127.0.0.1', 8080, config.make_wsgi_app())
|
||||
print("serving")
|
||||
server.serve_forever()
|
||||
Reference in New Issue
Block a user