-
Notifications
You must be signed in to change notification settings - Fork 1.7k
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
Python: Add SSRF queries #7420
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
5de79b4
Python: Add HTTP::Client::Request concept
RasmusWL 08f6d1a
Python: Clearer sourceType for client response body
RasmusWL 7bf285a
Python: Alter `disablesCertificateValidation` to fit our needs
RasmusWL 1ff56d5
Python: Add tests of `requests`
RasmusWL b68d280
Python: Add modeling of `requests`
RasmusWL 35cba17
Python: Consider taint of client http requests
RasmusWL cf2ee06
Python: Model `requests` Responses
RasmusWL a5bae30
Python: Add tests of `http.client.HTTPResponse`
RasmusWL 6f81685
Python: Add modeling of `http.client.HTTPResponse`
RasmusWL f8fc583
Python: client request: getUrl => getAUrlPart
RasmusWL 579de0c
Python: Remove `getResponse` and do manual taint steps
RasmusWL 1cc5e54
Python: Add SSRF queries
RasmusWL 6ce1524
Python: Apply suggestions from code review
RasmusWL 5a7efd0
Python: Minor adjustments to QLDoc of `HTTP::Client::Request`
RasmusWL b1bca85
Python: Add interesting test-case
RasmusWL cb934e1
Python: Adjust SSRF location to request call
RasmusWL 4b5599f
Python: Improve full/partial SSRF split
RasmusWL 6f297f4
Python: Fix SSRF sanitizer tests
RasmusWL 8d9a797
Python: Add tricky .format SSRF tests
RasmusWL 1d00730
Python: Allow `http[s]://` prefix for SSRF
RasmusWL e309d82
Python: Remove debug predicate
RasmusWL e7abe43
Python: Add SSRF change-note
RasmusWL 83f1b2c
Python: Add SSRF qhelp
RasmusWL 9866214
Update python/ql/test/query-tests/Security/CWE-918-ServerSideRequestF…
yoff 626009e
Python: Fix typo
RasmusWL 83f87f0
Python: Adjust .expected based on new comment
RasmusWL File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
lgtm,codescanning | ||
* Two new queries have been added for detecting Server-side request forgery (SSRF). _Full server-side request forgery_ (`py/full-ssrf`) will only alert when the URL is fully user-controlled, and _Partial server-side request forgery_ (`py/partial-ssrf`) will alert when any part of the URL is user-controlled. Only `py/full-ssrf` will be run by default. | ||
* To support the new SSRF queries, the PyPI package `requests` have been modeled, along with `http.client.HTTP[S]Connection` from the standard library. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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" | ||
} | ||
} | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.