Skip to content

Commit 8e68eae

Browse files
authored
Merge pull request #5463 from jorgectf/jorgectf/python/headerInjection
Python: Add Header Injection query
2 parents 493a37b + 271e2e4 commit 8e68eae

File tree

13 files changed

+445
-0
lines changed

13 files changed

+445
-0
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
<overview>
6+
<p>If an HTTP Header is built using string concatenation or string formatting, and the
7+
components of the concatenation include user input, a user
8+
is likely to be able to manipulate the response.</p>
9+
</overview>
10+
11+
<recommendation>
12+
<p>User input should not be included in an HTTP Header.</p>
13+
</recommendation>
14+
15+
<example>
16+
<p>In the following example, the code appends a user-provided value into a header.</p>
17+
18+
<sample src="header_injection.py" />
19+
</example>
20+
21+
<references>
22+
<li>OWASP: <a href="https://owasp.org/www-community/attacks/HTTP_Response_Splitting">HTTP Response Splitting</a>.</li>
23+
<li>Python Security: <a href="https://python-security.readthedocs.io/vuln/http-header-injection.html">HTTP header injection</a>.</li>
24+
<li>SonarSource: <a href="https://rules.sonarsource.com/python/RSPEC-5167">RSPEC-5167</a>.</li>
25+
</references>
26+
</qhelp>
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**
2+
* @name HTTP Header Injection
3+
* @description User input should not be used in HTTP headers, otherwise a malicious user
4+
* may be able to inject a value that could manipulate the response.
5+
* @kind path-problem
6+
* @problem.severity error
7+
* @id py/header-injection
8+
* @tags security
9+
* external/cwe/cwe-113
10+
* external/cwe/cwe-079
11+
*/
12+
13+
// determine precision above
14+
import python
15+
import experimental.semmle.python.security.injection.HTTPHeaders
16+
import DataFlow::PathGraph
17+
18+
from HeaderInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink
19+
where config.hasFlowPath(source, sink)
20+
select sink.getNode(), source, sink, "$@ HTTP header is constructed from a $@.", sink.getNode(),
21+
"This", source.getNode(), "user-provided value"
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from flask import Response, request, Flask, make_response
2+
3+
4+
@app.route("/flask_Response")
5+
def flask_Response():
6+
rfs_header = request.args["rfs_header"]
7+
response = Response()
8+
response.headers['HeaderName'] = rfs_header
9+
return response

python/ql/src/experimental/semmle/python/Concepts.qll

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,3 +253,46 @@ class NoSQLSanitizer extends DataFlow::Node {
253253
/** Gets the argument that specifies the NoSQL query to be sanitized. */
254254
DataFlow::Node getAnInput() { result = range.getAnInput() }
255255
}
256+
257+
/** Provides classes for modeling HTTP Header APIs. */
258+
module HeaderDeclaration {
259+
/**
260+
* A data-flow node that collects functions setting HTTP Headers.
261+
*
262+
* Extend this class to model new APIs. If you want to refine existing API models,
263+
* extend `HeaderDeclaration` instead.
264+
*/
265+
abstract class Range extends DataFlow::Node {
266+
/**
267+
* Gets the argument containing the header name.
268+
*/
269+
abstract DataFlow::Node getNameArg();
270+
271+
/**
272+
* Gets the argument containing the header value.
273+
*/
274+
abstract DataFlow::Node getValueArg();
275+
}
276+
}
277+
278+
/**
279+
* A data-flow node that collects functions setting HTTP Headers.
280+
*
281+
* Extend this class to refine existing API models. If you want to model new APIs,
282+
* extend `HeaderDeclaration::Range` instead.
283+
*/
284+
class HeaderDeclaration extends DataFlow::Node {
285+
HeaderDeclaration::Range range;
286+
287+
HeaderDeclaration() { this = range }
288+
289+
/**
290+
* Gets the argument containing the header name.
291+
*/
292+
DataFlow::Node getNameArg() { result = range.getNameArg() }
293+
294+
/**
295+
* Gets the argument containing the header value.
296+
*/
297+
DataFlow::Node getValueArg() { result = range.getValueArg() }
298+
}

python/ql/src/experimental/semmle/python/Frameworks.qll

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
*/
44

55
private import experimental.semmle.python.frameworks.Stdlib
6+
private import experimental.semmle.python.frameworks.Flask
7+
private import experimental.semmle.python.frameworks.Django
8+
private import experimental.semmle.python.frameworks.Werkzeug
69
private import experimental.semmle.python.frameworks.LDAP
710
private import experimental.semmle.python.frameworks.NoSQL
811
private import experimental.semmle.python.frameworks.Log
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `django` PyPI package.
3+
* See https://www.djangoproject.com/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.frameworks.Django
8+
private import semmle.python.dataflow.new.DataFlow
9+
private import experimental.semmle.python.Concepts
10+
private import semmle.python.ApiGraphs
11+
import semmle.python.dataflow.new.RemoteFlowSources
12+
13+
private module PrivateDjango {
14+
private module django {
15+
API::Node http() { result = API::moduleImport("django").getMember("http") }
16+
17+
module http {
18+
API::Node response() { result = http().getMember("response") }
19+
20+
API::Node request() { result = http().getMember("request") }
21+
22+
module request {
23+
module HttpRequest {
24+
class DjangoGETParameter extends DataFlow::Node, RemoteFlowSource::Range {
25+
DjangoGETParameter() { this = request().getMember("GET").getMember("get").getACall() }
26+
27+
override string getSourceType() { result = "django.http.request.GET.get" }
28+
}
29+
}
30+
}
31+
32+
module response {
33+
module HttpResponse {
34+
API::Node baseClassRef() {
35+
result = response().getMember("HttpResponse").getReturn()
36+
or
37+
// Handle `django.http.HttpResponse` alias
38+
result = http().getMember("HttpResponse").getReturn()
39+
}
40+
41+
/** Gets a reference to a header instance. */
42+
private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) {
43+
t.start() and
44+
(
45+
exists(SubscriptNode subscript |
46+
subscript.getObject() = baseClassRef().getAUse().asCfgNode() and
47+
result.asCfgNode() = subscript
48+
)
49+
or
50+
result.(DataFlow::AttrRead).getObject() = baseClassRef().getAUse()
51+
)
52+
or
53+
exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t))
54+
}
55+
56+
/** Gets a reference to a header instance use. */
57+
private DataFlow::Node headerInstance() {
58+
headerInstance(DataFlow::TypeTracker::end()).flowsTo(result)
59+
}
60+
61+
/** Gets a reference to a header instance call with `__setitem__`. */
62+
private DataFlow::Node headerSetItemCall() {
63+
result = headerInstance() and
64+
result.(DataFlow::AttrRead).getAttributeName() = "__setitem__"
65+
}
66+
67+
class DjangoResponseSetItemCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
68+
DjangoResponseSetItemCall() { this.getFunction() = headerSetItemCall() }
69+
70+
override DataFlow::Node getNameArg() { result = this.getArg(0) }
71+
72+
override DataFlow::Node getValueArg() { result = this.getArg(1) }
73+
}
74+
75+
class DjangoResponseDefinition extends DataFlow::Node, HeaderDeclaration::Range {
76+
DataFlow::Node headerInput;
77+
78+
DjangoResponseDefinition() {
79+
this.asCfgNode().(DefinitionNode) = headerInstance().asCfgNode() and
80+
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
81+
}
82+
83+
override DataFlow::Node getNameArg() {
84+
result.asExpr() = this.asExpr().(Subscript).getIndex()
85+
}
86+
87+
override DataFlow::Node getValueArg() { result = headerInput }
88+
}
89+
}
90+
}
91+
}
92+
}
93+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `flask` PyPI package.
3+
* See https://flask.palletsprojects.com/en/1.1.x/.
4+
*/
5+
6+
private import python
7+
private import semmle.python.frameworks.Flask
8+
private import semmle.python.dataflow.new.DataFlow
9+
private import experimental.semmle.python.Concepts
10+
private import semmle.python.ApiGraphs
11+
12+
module ExperimentalFlask {
13+
/**
14+
* A reference to either `flask.make_response` function, or the `make_response` method on
15+
* an instance of `flask.Flask`. This creates an instance of the `flask_response`
16+
* class (class-attribute on a flask application), which by default is
17+
* `flask.Response`.
18+
*
19+
* See
20+
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.Flask.make_response
21+
* - https://flask.palletsprojects.com/en/1.1.x/api/#flask.make_response
22+
*/
23+
private API::Node flaskMakeResponse() {
24+
result =
25+
[API::moduleImport("flask"), Flask::FlaskApp::instance()]
26+
.getMember(["make_response", "jsonify", "make_default_options_response"])
27+
}
28+
29+
/** Gets a reference to a header instance. */
30+
private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) {
31+
t.start() and
32+
result.(DataFlow::AttrRead).getObject().getALocalSource() =
33+
[Flask::Response::classRef(), flaskMakeResponse()].getReturn().getAUse()
34+
or
35+
exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t))
36+
}
37+
38+
/** Gets a reference to a header instance use. */
39+
private DataFlow::Node headerInstance() {
40+
headerInstance(DataFlow::TypeTracker::end()).flowsTo(result)
41+
}
42+
43+
/** Gets a reference to a header instance call/subscript */
44+
private DataFlow::Node headerInstanceCall() {
45+
headerInstance() in [result.(DataFlow::AttrRead), result.(DataFlow::AttrRead).getObject()] or
46+
headerInstance().asExpr() = result.asExpr().(Subscript).getObject()
47+
}
48+
49+
class FlaskHeaderDefinition extends DataFlow::Node, HeaderDeclaration::Range {
50+
DataFlow::Node headerInput;
51+
52+
FlaskHeaderDefinition() {
53+
this.asCfgNode().(DefinitionNode) = headerInstanceCall().asCfgNode() and
54+
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
55+
}
56+
57+
override DataFlow::Node getNameArg() { result.asExpr() = this.asExpr().(Subscript).getIndex() }
58+
59+
override DataFlow::Node getValueArg() { result = headerInput }
60+
}
61+
62+
private class FlaskMakeResponseExtend extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
63+
KeyValuePair item;
64+
65+
FlaskMakeResponseExtend() {
66+
this.getFunction() = headerInstanceCall() and
67+
item = this.getArg(_).asExpr().(Dict).getAnItem()
68+
}
69+
70+
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
71+
72+
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
73+
}
74+
75+
private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
76+
KeyValuePair item;
77+
78+
FlaskResponse() { this = Flask::Response::classRef().getACall() }
79+
80+
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
81+
82+
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
83+
}
84+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
/**
2+
* Provides classes modeling security-relevant aspects of the `Werkzeug` PyPI package.
3+
* See
4+
* - https://pypi.org/project/Werkzeug/
5+
* - https://werkzeug.palletsprojects.com/en/1.0.x/#werkzeug
6+
*/
7+
8+
private import python
9+
private import semmle.python.frameworks.Flask
10+
private import semmle.python.dataflow.new.DataFlow
11+
private import experimental.semmle.python.Concepts
12+
private import semmle.python.ApiGraphs
13+
14+
private module Werkzeug {
15+
module datastructures {
16+
module Headers {
17+
class WerkzeugHeaderAddCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
18+
WerkzeugHeaderAddCall() {
19+
this.getFunction().(DataFlow::AttrRead).getObject().getALocalSource() =
20+
API::moduleImport("werkzeug")
21+
.getMember("datastructures")
22+
.getMember("Headers")
23+
.getACall() and
24+
this.getFunction().(DataFlow::AttrRead).getAttributeName() = "add"
25+
}
26+
27+
override DataFlow::Node getNameArg() { result = this.getArg(0) }
28+
29+
override DataFlow::Node getValueArg() { result = this.getArg(1) }
30+
}
31+
}
32+
}
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import python
2+
import experimental.semmle.python.Concepts
3+
import semmle.python.dataflow.new.DataFlow
4+
import semmle.python.dataflow.new.TaintTracking
5+
import semmle.python.dataflow.new.RemoteFlowSources
6+
7+
/**
8+
* A taint-tracking configuration for detecting HTTP Header injections.
9+
*/
10+
class HeaderInjectionFlowConfig extends TaintTracking::Configuration {
11+
HeaderInjectionFlowConfig() { this = "HeaderInjectionFlowConfig" }
12+
13+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
14+
15+
override predicate isSink(DataFlow::Node sink) {
16+
exists(HeaderDeclaration headerDeclaration |
17+
sink in [headerDeclaration.getNameArg(), headerDeclaration.getValueArg()]
18+
)
19+
}
20+
}

0 commit comments

Comments
 (0)