Skip to content

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
merged 26 commits into from
Oct 18, 2021
Merged
Show file tree
Hide file tree
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 Mar 19, 2021
46c5cb1
Polish WerkzeugHeaderCall
jorgectf Apr 1, 2021
3be916e
Polish FlaskHeaderCall
jorgectf Apr 1, 2021
bd894ae
Fix flask test
jorgectf Apr 3, 2021
6158dd6
Finish Sinks
jorgectf Apr 3, 2021
b0c4986
Init restructuring
jorgectf Apr 8, 2021
ce3fb6b
Improve qhelp
jorgectf Apr 8, 2021
789c585
Create qhelp example
jorgectf Apr 8, 2021
e9c4574
Apply structure
jorgectf Apr 8, 2021
632dc61
Create qlref
jorgectf Apr 8, 2021
f02c285
Generate .expected
jorgectf Apr 8, 2021
066504e
Checkout Stdlib.qll
jorgectf Jun 18, 2021
4963caf
Rewrite frameworks modeling
jorgectf Jun 18, 2021
dcb1da3
Extend documentation
jorgectf Jun 18, 2021
eac5254
Resolve merge conflict
jorgectf Jun 18, 2021
017a778
Polish make_response and fix extend argument
jorgectf Jun 18, 2021
b10ade1
Update HeaderDeclaration input naming
jorgectf Jun 19, 2021
8d0386b
Split into `getNameArg` and `getValueArg`
jorgectf Jul 25, 2021
190bc2f
Apply suggestions from code review
jorgectf Sep 7, 2021
352eab0
Fix `HeaderDeclaration` class' comment
jorgectf Sep 7, 2021
eee9b3f
Merge remote-tracking branch 'origin/main' into jorgectf/python/heade…
jorgectf Sep 7, 2021
2db1ffe
Merge remote-tracking branch 'origin/main' into jorgectf/python/heade…
jorgectf Oct 16, 2021
bf76d9c
Fix django test
jorgectf Oct 16, 2021
45146bc
Merge branch 'main' into jorgectf/python/headerInjection
jorgectf Oct 16, 2021
14c50e9
Add django `GET.get` RFS
jorgectf Oct 16, 2021
271e2e4
Update `.expected`
jorgectf Oct 16, 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
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 python/ql/src/experimental/Security/CWE-113/HeaderInjection.ql
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"
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
43 changes: 43 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -253,3 +253,46 @@ class NoSQLSanitizer extends DataFlow::Node {
/** Gets the argument that specifies the NoSQL query to be sanitized. */
DataFlow::Node getAnInput() { result = range.getAnInput() }
}

/** Provides classes for modeling HTTP Header APIs. */
module HeaderDeclaration {
/**
* A data-flow node that collects functions setting HTTP Headers.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `HeaderDeclaration` instead.
*/
abstract class Range extends DataFlow::Node {
/**
* Gets the argument containing the header name.
*/
abstract DataFlow::Node getNameArg();

/**
* Gets the argument containing the header value.
*/
abstract DataFlow::Node getValueArg();
}
}

/**
* A data-flow node that collects functions setting HTTP Headers.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `HeaderDeclaration::Range` instead.
*/
class HeaderDeclaration extends DataFlow::Node {
HeaderDeclaration::Range range;

HeaderDeclaration() { this = range }

/**
* Gets the argument containing the header name.
*/
DataFlow::Node getNameArg() { result = range.getNameArg() }

/**
* Gets the argument containing the header value.
*/
DataFlow::Node getValueArg() { result = range.getValueArg() }
}
3 changes: 3 additions & 0 deletions python/ql/src/experimental/semmle/python/Frameworks.qll
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
*/

private import experimental.semmle.python.frameworks.Stdlib
private import experimental.semmle.python.frameworks.Flask
private import experimental.semmle.python.frameworks.Django
private import experimental.semmle.python.frameworks.Werkzeug
private import experimental.semmle.python.frameworks.LDAP
private import experimental.semmle.python.frameworks.NoSQL
private import experimental.semmle.python.frameworks.Log
93 changes: 93 additions & 0 deletions python/ql/src/experimental/semmle/python/frameworks/Django.qll
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 python/ql/src/experimental/semmle/python/frameworks/Flask.qll
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() }
}
}
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) }
}
}
}
}
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()]
)
}
}
Loading