-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Python: Add Header Injection query #5463
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
tausbn
merged 26 commits into
github:main
from
jorgectf:jorgectf/python/headerInjection
Oct 18, 2021
Merged
Changes from all commits
Commits
Show all changes
26 commits
Select commit
Hold shift + click to select a range
6f89b3f
Init Header Injection query
jorgectf 46c5cb1
Polish WerkzeugHeaderCall
jorgectf 3be916e
Polish FlaskHeaderCall
jorgectf bd894ae
Fix flask test
jorgectf 6158dd6
Finish Sinks
jorgectf b0c4986
Init restructuring
jorgectf ce3fb6b
Improve qhelp
jorgectf 789c585
Create qhelp example
jorgectf e9c4574
Apply structure
jorgectf 632dc61
Create qlref
jorgectf f02c285
Generate .expected
jorgectf 066504e
Checkout Stdlib.qll
jorgectf 4963caf
Rewrite frameworks modeling
jorgectf dcb1da3
Extend documentation
jorgectf eac5254
Resolve merge conflict
jorgectf 017a778
Polish make_response and fix extend argument
jorgectf b10ade1
Update HeaderDeclaration input naming
jorgectf 8d0386b
Split into `getNameArg` and `getValueArg`
jorgectf 190bc2f
Apply suggestions from code review
jorgectf 352eab0
Fix `HeaderDeclaration` class' comment
jorgectf eee9b3f
Merge remote-tracking branch 'origin/main' into jorgectf/python/heade…
jorgectf 2db1ffe
Merge remote-tracking branch 'origin/main' into jorgectf/python/heade…
jorgectf bf76d9c
Fix django test
jorgectf 45146bc
Merge branch 'main' into jorgectf/python/headerInjection
jorgectf 14c50e9
Add django `GET.get` RFS
jorgectf 271e2e4
Update `.expected`
jorgectf 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
26 changes: 26 additions & 0 deletions
26
python/ql/src/experimental/Security/CWE-113/HeaderInjection.qhelp
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,26 @@ | ||
<!DOCTYPE qhelp PUBLIC | ||
"-//Semmle//qhelp//EN" | ||
"qhelp.dtd"> | ||
<qhelp> | ||
<overview> | ||
<p>If an HTTP Header is built using string concatenation or string formatting, and the | ||
components of the concatenation include user input, a user | ||
is likely to be able to manipulate the response.</p> | ||
</overview> | ||
|
||
<recommendation> | ||
<p>User input should not be included in an HTTP Header.</p> | ||
</recommendation> | ||
|
||
<example> | ||
<p>In the following example, the code appends a user-provided value into a header.</p> | ||
|
||
<sample src="header_injection.py" /> | ||
</example> | ||
|
||
<references> | ||
<li>OWASP: <a href="https://owasp.org/www-community/attacks/HTTP_Response_Splitting">HTTP Response Splitting</a>.</li> | ||
<li>Python Security: <a href="https://python-security.readthedocs.io/vuln/http-header-injection.html">HTTP header injection</a>.</li> | ||
<li>SonarSource: <a href="https://rules.sonarsource.com/python/RSPEC-5167">RSPEC-5167</a>.</li> | ||
</references> | ||
</qhelp> |
21 changes: 21 additions & 0 deletions
21
python/ql/src/experimental/Security/CWE-113/HeaderInjection.ql
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,21 @@ | ||
/** | ||
* @name HTTP Header Injection | ||
* @description User input should not be used in HTTP headers, otherwise a malicious user | ||
* may be able to inject a value that could manipulate the response. | ||
* @kind path-problem | ||
* @problem.severity error | ||
* @id py/header-injection | ||
* @tags security | ||
* external/cwe/cwe-113 | ||
* external/cwe/cwe-079 | ||
*/ | ||
|
||
// determine precision above | ||
import python | ||
import experimental.semmle.python.security.injection.HTTPHeaders | ||
import DataFlow::PathGraph | ||
|
||
from HeaderInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink | ||
where config.hasFlowPath(source, sink) | ||
select sink.getNode(), source, sink, "$@ HTTP header is constructed from a $@.", sink.getNode(), | ||
"This", source.getNode(), "user-provided value" |
9 changes: 9 additions & 0 deletions
9
python/ql/src/experimental/Security/CWE-113/header_injection.py
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,9 @@ | ||
from flask import Response, request, Flask, make_response | ||
|
||
|
||
@app.route("/flask_Response") | ||
def flask_Response(): | ||
rfs_header = request.args["rfs_header"] | ||
response = Response() | ||
response.headers['HeaderName'] = rfs_header | ||
return response |
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
93 changes: 93 additions & 0 deletions
93
python/ql/src/experimental/semmle/python/frameworks/Django.qll
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,93 @@ | ||
/** | ||
* Provides classes modeling security-relevant aspects of the `django` PyPI package. | ||
* See https://www.djangoproject.com/. | ||
*/ | ||
|
||
private import python | ||
private import semmle.python.frameworks.Django | ||
private import semmle.python.dataflow.new.DataFlow | ||
private import experimental.semmle.python.Concepts | ||
private import semmle.python.ApiGraphs | ||
import semmle.python.dataflow.new.RemoteFlowSources | ||
|
||
private module PrivateDjango { | ||
private module django { | ||
API::Node http() { result = API::moduleImport("django").getMember("http") } | ||
|
||
module http { | ||
API::Node response() { result = http().getMember("response") } | ||
|
||
API::Node request() { result = http().getMember("request") } | ||
|
||
module request { | ||
module HttpRequest { | ||
class DjangoGETParameter extends DataFlow::Node, RemoteFlowSource::Range { | ||
DjangoGETParameter() { this = request().getMember("GET").getMember("get").getACall() } | ||
|
||
override string getSourceType() { result = "django.http.request.GET.get" } | ||
} | ||
} | ||
} | ||
|
||
module response { | ||
module HttpResponse { | ||
API::Node baseClassRef() { | ||
result = response().getMember("HttpResponse").getReturn() | ||
or | ||
// Handle `django.http.HttpResponse` alias | ||
result = http().getMember("HttpResponse").getReturn() | ||
} | ||
|
||
/** Gets a reference to a header instance. */ | ||
private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) { | ||
t.start() and | ||
( | ||
exists(SubscriptNode subscript | | ||
subscript.getObject() = baseClassRef().getAUse().asCfgNode() and | ||
result.asCfgNode() = subscript | ||
) | ||
or | ||
result.(DataFlow::AttrRead).getObject() = baseClassRef().getAUse() | ||
) | ||
or | ||
exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t)) | ||
} | ||
|
||
/** Gets a reference to a header instance use. */ | ||
private DataFlow::Node headerInstance() { | ||
headerInstance(DataFlow::TypeTracker::end()).flowsTo(result) | ||
} | ||
|
||
/** Gets a reference to a header instance call with `__setitem__`. */ | ||
private DataFlow::Node headerSetItemCall() { | ||
result = headerInstance() and | ||
result.(DataFlow::AttrRead).getAttributeName() = "__setitem__" | ||
} | ||
|
||
class DjangoResponseSetItemCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range { | ||
DjangoResponseSetItemCall() { this.getFunction() = headerSetItemCall() } | ||
|
||
override DataFlow::Node getNameArg() { result = this.getArg(0) } | ||
|
||
override DataFlow::Node getValueArg() { result = this.getArg(1) } | ||
} | ||
|
||
class DjangoResponseDefinition extends DataFlow::Node, HeaderDeclaration::Range { | ||
DataFlow::Node headerInput; | ||
|
||
DjangoResponseDefinition() { | ||
this.asCfgNode().(DefinitionNode) = headerInstance().asCfgNode() and | ||
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue() | ||
} | ||
|
||
override DataFlow::Node getNameArg() { | ||
result.asExpr() = this.asExpr().(Subscript).getIndex() | ||
} | ||
|
||
override DataFlow::Node getValueArg() { result = headerInput } | ||
} | ||
} | ||
} | ||
} | ||
} | ||
} |
84 changes: 84 additions & 0 deletions
84
python/ql/src/experimental/semmle/python/frameworks/Flask.qll
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,84 @@ | ||
/** | ||
* Provides classes modeling security-relevant aspects of the `flask` PyPI package. | ||
* See https://flask.palletsprojects.com/en/1.1.x/. | ||
*/ | ||
|
||
private import python | ||
private import semmle.python.frameworks.Flask | ||
private import semmle.python.dataflow.new.DataFlow | ||
private import experimental.semmle.python.Concepts | ||
private import semmle.python.ApiGraphs | ||
|
||
module ExperimentalFlask { | ||
/** | ||
* A reference to either `flask.make_response` function, or the `make_response` method on | ||
* an instance of `flask.Flask`. This creates an instance of the `flask_response` | ||
* class (class-attribute on a flask application), which by default is | ||
* `flask.Response`. | ||
* | ||
* See | ||
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.make_response | ||
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response | ||
*/ | ||
private API::Node flaskMakeResponse() { | ||
result = | ||
[API::moduleImport("flask"), Flask::FlaskApp::instance()] | ||
.getMember(["make_response", "jsonify", "make_default_options_response"]) | ||
} | ||
|
||
/** Gets a reference to a header instance. */ | ||
private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) { | ||
t.start() and | ||
result.(DataFlow::AttrRead).getObject().getALocalSource() = | ||
[Flask::Response::classRef(), flaskMakeResponse()].getReturn().getAUse() | ||
or | ||
exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t)) | ||
} | ||
|
||
/** Gets a reference to a header instance use. */ | ||
private DataFlow::Node headerInstance() { | ||
headerInstance(DataFlow::TypeTracker::end()).flowsTo(result) | ||
} | ||
|
||
/** Gets a reference to a header instance call/subscript */ | ||
private DataFlow::Node headerInstanceCall() { | ||
headerInstance() in [result.(DataFlow::AttrRead), result.(DataFlow::AttrRead).getObject()] or | ||
headerInstance().asExpr() = result.asExpr().(Subscript).getObject() | ||
} | ||
|
||
class FlaskHeaderDefinition extends DataFlow::Node, HeaderDeclaration::Range { | ||
DataFlow::Node headerInput; | ||
|
||
FlaskHeaderDefinition() { | ||
this.asCfgNode().(DefinitionNode) = headerInstanceCall().asCfgNode() and | ||
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue() | ||
} | ||
|
||
override DataFlow::Node getNameArg() { result.asExpr() = this.asExpr().(Subscript).getIndex() } | ||
|
||
override DataFlow::Node getValueArg() { result = headerInput } | ||
} | ||
|
||
private class FlaskMakeResponseExtend extends DataFlow::CallCfgNode, HeaderDeclaration::Range { | ||
KeyValuePair item; | ||
|
||
FlaskMakeResponseExtend() { | ||
this.getFunction() = headerInstanceCall() and | ||
item = this.getArg(_).asExpr().(Dict).getAnItem() | ||
} | ||
|
||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() } | ||
|
||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() } | ||
} | ||
|
||
private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range { | ||
KeyValuePair item; | ||
|
||
FlaskResponse() { this = Flask::Response::classRef().getACall() } | ||
|
||
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() } | ||
|
||
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() } | ||
} | ||
} |
33 changes: 33 additions & 0 deletions
33
python/ql/src/experimental/semmle/python/frameworks/Werkzeug.qll
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,33 @@ | ||
/** | ||
* Provides classes modeling security-relevant aspects of the `Werkzeug` PyPI package. | ||
* See | ||
* - https://pypi.org/project/Werkzeug/ | ||
* - https://werkzeug.palletsprojects.com/en/1.0.x/#werkzeug | ||
*/ | ||
|
||
private import python | ||
private import semmle.python.frameworks.Flask | ||
private import semmle.python.dataflow.new.DataFlow | ||
private import experimental.semmle.python.Concepts | ||
private import semmle.python.ApiGraphs | ||
|
||
private module Werkzeug { | ||
module datastructures { | ||
module Headers { | ||
class WerkzeugHeaderAddCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range { | ||
WerkzeugHeaderAddCall() { | ||
this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() = | ||
API::moduleImport("werkzeug") | ||
.getMember("datastructures") | ||
.getMember("Headers") | ||
.getACall() and | ||
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "add" | ||
} | ||
|
||
override DataFlow::Node getNameArg() { result = this.getArg(0) } | ||
|
||
override DataFlow::Node getValueArg() { result = this.getArg(1) } | ||
} | ||
} | ||
} | ||
} |
20 changes: 20 additions & 0 deletions
20
python/ql/src/experimental/semmle/python/security/injection/HTTPHeaders.qll
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,20 @@ | ||
import python | ||
import experimental.semmle.python.Concepts | ||
import semmle.python.dataflow.new.DataFlow | ||
import semmle.python.dataflow.new.TaintTracking | ||
import semmle.python.dataflow.new.RemoteFlowSources | ||
|
||
/** | ||
* A taint-tracking configuration for detecting HTTP Header injections. | ||
*/ | ||
class HeaderInjectionFlowConfig extends TaintTracking::Configuration { | ||
HeaderInjectionFlowConfig() { this = "HeaderInjectionFlowConfig" } | ||
|
||
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource } | ||
|
||
override predicate isSink(DataFlow::Node sink) { | ||
exists(HeaderDeclaration headerDeclaration | | ||
sink in [headerDeclaration.getNameArg(), headerDeclaration.getValueArg()] | ||
) | ||
} | ||
} |
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.