Skip to content

Python: Add SSRF queries #7420

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 26 commits into from
Dec 17, 2021
Merged
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
5de79b4
Python: Add HTTP::Client::Request concept
RasmusWL Dec 13, 2021
08f6d1a
Python: Clearer sourceType for client response body
RasmusWL Dec 13, 2021
7bf285a
Python: Alter `disablesCertificateValidation` to fit our needs
RasmusWL Dec 13, 2021
1ff56d5
Python: Add tests of `requests`
RasmusWL Dec 13, 2021
b68d280
Python: Add modeling of `requests`
RasmusWL Dec 13, 2021
35cba17
Python: Consider taint of client http requests
RasmusWL Dec 13, 2021
cf2ee06
Python: Model `requests` Responses
RasmusWL Dec 13, 2021
a5bae30
Python: Add tests of `http.client.HTTPResponse`
RasmusWL Dec 15, 2021
6f81685
Python: Add modeling of `http.client.HTTPResponse`
RasmusWL Dec 15, 2021
f8fc583
Python: client request: getUrl => getAUrlPart
RasmusWL Dec 15, 2021
579de0c
Python: Remove `getResponse` and do manual taint steps
RasmusWL Dec 15, 2021
1cc5e54
Python: Add SSRF queries
RasmusWL Dec 16, 2021
6ce1524
Python: Apply suggestions from code review
RasmusWL Dec 16, 2021
5a7efd0
Python: Minor adjustments to QLDoc of `HTTP::Client::Request`
RasmusWL Dec 16, 2021
b1bca85
Python: Add interesting test-case
RasmusWL Dec 16, 2021
cb934e1
Python: Adjust SSRF location to request call
RasmusWL Dec 16, 2021
4b5599f
Python: Improve full/partial SSRF split
RasmusWL Dec 16, 2021
6f297f4
Python: Fix SSRF sanitizer tests
RasmusWL Dec 16, 2021
8d9a797
Python: Add tricky .format SSRF tests
RasmusWL Dec 16, 2021
1d00730
Python: Allow `http[s]://` prefix for SSRF
RasmusWL Dec 16, 2021
e309d82
Python: Remove debug predicate
RasmusWL Dec 17, 2021
e7abe43
Python: Add SSRF change-note
RasmusWL Dec 17, 2021
83f1b2c
Python: Add SSRF qhelp
RasmusWL Dec 17, 2021
9866214
Update python/ql/test/query-tests/Security/CWE-918-ServerSideRequestF…
yoff Dec 17, 2021
626009e
Python: Fix typo
RasmusWL Dec 17, 2021
83f87f0
Python: Adjust .expected based on new comment
RasmusWL Dec 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/codeql/support/reusables/frameworks.rst
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,7 @@ Python built-in support
Twisted, Web framework
Flask-Admin, Web framework
starlette, Asynchronous Server Gateway Interface (ASGI)
requests, HTTP client
dill, Serialization
PyYAML, Serialization
ruamel.yaml, Serialization
Expand Down
66 changes: 66 additions & 0 deletions python/ql/lib/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -812,6 +812,72 @@ module HTTP {
}
}
}

/** Provides classes for modeling HTTP clients. */
module Client {
/**
* A data-flow node that makes an outgoing HTTP request.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `HTTP::Client::Request::Range` instead.
*/
class Request extends DataFlow::Node instanceof Request::Range {
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
DataFlow::Node getAUrlPart() { result = super.getAUrlPart() }

/** Gets a string that identifies the framework used for this request. */
string getFramework() { result = super.getFramework() }

/**
* Holds if this request is made using a mode that disables SSL/TLS
* certificate validation, where `disablingNode` represents the point at
* which the validation was disabled, and `argumentOrigin` represents the origin
* of the argument that disabled the validation (which could be the same node as
* `disablingNode`).
*/
predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
super.disablesCertificateValidation(disablingNode, argumentOrigin)
}
}

/** Provides a class for modeling new HTTP requests. */
module Request {
/**
* A data-flow node that makes an outgoing HTTP request.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `HTTP::Client::Request` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets a data-flow node that contributes to the URL of the request.
* Depending on the framework, a request may have multiple nodes which contribute to the URL.
*/
abstract DataFlow::Node getAUrlPart();

/** Gets a string that identifies the framework used for this request. */
abstract string getFramework();

/**
* Holds if this request is made using a mode that disables SSL/TLS
* certificate validation, where `disablingNode` represents the point at
* which the validation was disabled, and `argumentOrigin` represents the origin
* of the argument that disabled the validation (which could be the same node as
* `disablingNode`).
*/
abstract predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
);
}
}
// TODO: investigate whether we should treat responses to client requests as
// remote-flow-sources in general.
}
}

/**
Expand Down
1 change: 1 addition & 0 deletions python/ql/lib/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ private import semmle.python.frameworks.Peewee
private import semmle.python.frameworks.Psycopg2
private import semmle.python.frameworks.Pydantic
private import semmle.python.frameworks.PyMySQL
private import semmle.python.frameworks.Requests
private import semmle.python.frameworks.RestFramework
private import semmle.python.frameworks.Rsa
private import semmle.python.frameworks.RuamelYaml
Expand Down
171 changes: 171 additions & 0 deletions python/ql/lib/semmle/python/frameworks/Requests.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,171 @@
/**
* Provides classes modeling security-relevant aspects of the `requests` PyPI package.
*
* See
* - https://pypi.org/project/requests/
* - https://docs.python-requests.org/en/latest/
*/

private import python
private import semmle.python.Concepts
private import semmle.python.ApiGraphs
private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.frameworks.internal.InstanceTaintStepsHelper
private import semmle.python.frameworks.Stdlib

/**
* INTERNAL: Do not use.
*
* Provides models for the `requests` PyPI package.
*
* See
* - https://pypi.org/project/requests/
* - https://docs.python-requests.org/en/latest/
*/
private module Requests {
private class OutgoingRequestCall extends HTTP::Client::Request::Range, DataFlow::CallCfgNode {
string methodName;

OutgoingRequestCall() {
methodName in [HTTP::httpVerbLower(), "request"] and
(
this = API::moduleImport("requests").getMember(methodName).getACall()
or
exists(API::Node moduleExporting, API::Node sessionInstance |
moduleExporting in [
API::moduleImport("requests"), //
API::moduleImport("requests").getMember("sessions")
] and
sessionInstance = moduleExporting.getMember(["Session", "session"]).getReturn()
|
this = sessionInstance.getMember(methodName).getACall()
)
)
}

override DataFlow::Node getAUrlPart() {
result = this.getArgByName("url")
or
not methodName = "request" and
result = this.getArg(0)
or
methodName = "request" and
result = this.getArg(1)
}

/** Gets the `verify` argument to this outgoing requests call. */
DataFlow::Node getVerifyArg() { result = this.getArgByName("verify") }

override predicate disablesCertificateValidation(
DataFlow::Node disablingNode, DataFlow::Node argumentOrigin
) {
disablingNode = this.getVerifyArg() and
argumentOrigin = verifyArgBacktracker(disablingNode) and
argumentOrigin.asExpr().(ImmutableLiteral).booleanValue() = false and
not argumentOrigin.asExpr() instanceof None
}

override string getFramework() { result = "requests" }
}

/**
* Extra taint propagation for outgoing requests calls,
* to ensure that responses to user-controlled URL are tainted.
*/
private class OutgoingRequestCallTaintStep extends TaintTracking::AdditionalTaintStep {
override predicate step(DataFlow::Node nodeFrom, DataFlow::Node nodeTo) {
nodeFrom = nodeTo.(OutgoingRequestCall).getAUrlPart()
}
}

/** Gets a back-reference to the verify argument `arg`. */
private DataFlow::TypeTrackingNode verifyArgBacktracker(
DataFlow::TypeBackTracker t, DataFlow::Node arg
) {
t.start() and
arg = any(OutgoingRequestCall c).getVerifyArg() and
result = arg.getALocalSource()
or
exists(DataFlow::TypeBackTracker t2 | result = verifyArgBacktracker(t2, arg).backtrack(t2, t))
}

/** Gets a back-reference to the verify argument `arg`. */
private DataFlow::LocalSourceNode verifyArgBacktracker(DataFlow::Node arg) {
result = verifyArgBacktracker(DataFlow::TypeBackTracker::end(), arg)
}

// ---------------------------------------------------------------------------
// Response
// ---------------------------------------------------------------------------
/**
* Provides models for the `requests.models.Response` class
*
* See https://docs.python-requests.org/en/latest/api/#requests.Response.
*/
module Response {
/** Gets a reference to the `requests.models.Response` class. */
private API::Node classRef() {
result = API::moduleImport("requests").getMember("models").getMember("Response")
or
result = API::moduleImport("requests").getMember("Response")
}

/**
* A source of instances of `requests.models.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 `requests.models.Response`.
*/
abstract class InstanceSource extends DataFlow::LocalSourceNode { }

/** A direct instantiation of `requests.models.Response`. */
private class ClassInstantiation extends InstanceSource, DataFlow::CallCfgNode {
ClassInstantiation() { this = classRef().getACall() }
}

/** Return value from making a reuqest. */
private class RequestReturnValue extends InstanceSource, DataFlow::Node {
RequestReturnValue() { this = any(OutgoingRequestCall c) }
}

/** Gets a reference to an instance of `requests.models.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 `requests.models.Response`. */
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }

/**
* Taint propagation for `requests.models.Response`.
*/
private class InstanceTaintSteps extends InstanceTaintStepsHelper {
InstanceTaintSteps() { this = "requests.models.Response" }

override DataFlow::Node getInstance() { result = instance() }

override string getAttributeName() {
result in ["text", "content", "raw", "links", "cookies", "headers"]
}

override string getMethodName() { result in ["json", "iter_content", "iter_lines"] }

override string getAsyncMethodName() { none() }
}

/** An attribute read that is a file-like instance. */
private class FileLikeInstances extends Stdlib::FileLikeObject::InstanceSource {
FileLikeInstances() {
this.(DataFlow::AttrRead).getObject() = instance() and
this.(DataFlow::AttrRead).getAttributeName() = "raw"
}
}
}
}
Loading