Skip to content

Commit 0aaa9c1

Browse files
committed
Merge remote-tracking branch 'origin/jorgectf/python/headerInjection' into jorgectf/python/insecure-cookie
2 parents 0e9d36b + b10ade1 commit 0aaa9c1

13 files changed

Lines changed: 398 additions & 0 deletions

File tree

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: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,36 @@ class SQLEscape extends DataFlow::Node {
209209
*/
210210
DataFlow::Node getAnInput() { result = range.getAnInput() }
211211
}
212+
213+
/** Provides classes for modeling HTTP Header APIs. */
214+
module HeaderDeclaration {
215+
/**
216+
* A data-flow node that collects functions setting HTTP Headers' content.
217+
*
218+
* Extend this class to model new APIs. If you want to refine existing API models,
219+
* extend `HeaderDeclaration` instead.
220+
*/
221+
abstract class Range extends DataFlow::Node {
222+
/**
223+
* Gets the argument containing the header value.
224+
*/
225+
abstract DataFlow::Node getAnInput();
226+
}
227+
}
228+
229+
/**
230+
* A data-flow node that collects functions setting HTTP Headers' content.
231+
*
232+
* Extend this class to model new APIs. If you want to refine existing API models,
233+
* extend `HeaderDeclaration` instead.
234+
*/
235+
class HeaderDeclaration extends DataFlow::Node {
236+
HeaderDeclaration::Range range;
237+
238+
HeaderDeclaration() { this = range }
239+
240+
/**
241+
* Gets the argument containing the header value.
242+
*/
243+
DataFlow::Node getAnInput() { result = range.getAnInput() }
244+
}

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,7 @@
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
Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
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+
12+
private module PrivateDjango {
13+
API::Node django() { result = API::moduleImport("django") }
14+
15+
private module django {
16+
API::Node http() { result = django().getMember("http") }
17+
18+
module http {
19+
API::Node response() { result = http().getMember("response") }
20+
21+
module response {
22+
module HttpResponse {
23+
API::Node baseClassRef() {
24+
result = response().getMember("HttpResponse").getReturn()
25+
or
26+
// Handle `django.http.HttpResponse` alias
27+
result = http().getMember("HttpResponse").getReturn()
28+
}
29+
30+
/** Gets a reference to a header instance. */
31+
private DataFlow::LocalSourceNode headerInstance(DataFlow::TypeTracker t) {
32+
t.start() and
33+
(
34+
exists(SubscriptNode subscript |
35+
subscript.getObject() = baseClassRef().getAUse().asCfgNode() and
36+
result.asCfgNode() = subscript
37+
)
38+
or
39+
result.(DataFlow::AttrRead).getObject() = baseClassRef().getAUse()
40+
)
41+
or
42+
exists(DataFlow::TypeTracker t2 | result = headerInstance(t2).track(t2, t))
43+
}
44+
45+
/** Gets a reference to a header instance use. */
46+
private DataFlow::Node headerInstance() {
47+
headerInstance(DataFlow::TypeTracker::end()).flowsTo(result)
48+
}
49+
50+
/** Gets a reference to a header instance call with `__setitem__`. */
51+
private DataFlow::Node headerSetItemCall() {
52+
result = headerInstance() and
53+
result.(DataFlow::AttrRead).getAttributeName() = "__setitem__"
54+
}
55+
56+
class DjangoResponseSetItemCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
57+
DjangoResponseSetItemCall() { this.getFunction() = headerSetItemCall() }
58+
59+
override DataFlow::Node getAnInput() { result = this.getArg([0, 1]) }
60+
}
61+
62+
class DjangoResponseDefinition extends DataFlow::Node, HeaderDeclaration::Range {
63+
DataFlow::Node headerInput;
64+
65+
DjangoResponseDefinition() {
66+
this.asCfgNode().(DefinitionNode) = headerInstance().asCfgNode() and
67+
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
68+
}
69+
70+
override DataFlow::Node getAnInput() {
71+
result.asExpr() in [headerInput.asExpr(), this.asExpr().(Subscript).getIndex()]
72+
}
73+
}
74+
}
75+
}
76+
}
77+
}
78+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 getAnInput() {
58+
result.asExpr() in [headerInput.asExpr(), this.asExpr().(Subscript).getIndex()]
59+
}
60+
}
61+
62+
private class FlaskMakeResponseExtend extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
63+
FlaskMakeResponseExtend() { this.getFunction() = headerInstanceCall() }
64+
65+
override DataFlow::Node getAnInput() { result = this.getArg(_) }
66+
}
67+
68+
private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
69+
FlaskResponse() { this = Flask::Response::classRef().getACall() }
70+
71+
override DataFlow::Node getAnInput() { result = this.getArgByName("headers") }
72+
}
73+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
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 getAnInput() { result = this.getArg(_) }
28+
}
29+
}
30+
}
31+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
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+
sink = any(HeaderDeclaration headerDeclaration).getAnInput()
17+
}
18+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
edges
2+
| flask_bad.py:9:18:9:24 | ControlFlowNode for request | flask_bad.py:9:18:9:29 | ControlFlowNode for Attribute |
3+
| flask_bad.py:9:18:9:29 | ControlFlowNode for Attribute | flask_bad.py:9:18:9:43 | ControlFlowNode for Subscript |
4+
| flask_bad.py:9:18:9:43 | ControlFlowNode for Subscript | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header |
5+
| flask_bad.py:19:18:19:24 | ControlFlowNode for request | flask_bad.py:19:18:19:29 | ControlFlowNode for Attribute |
6+
| flask_bad.py:19:18:19:29 | ControlFlowNode for Attribute | flask_bad.py:19:18:19:43 | ControlFlowNode for Subscript |
7+
| flask_bad.py:19:18:19:43 | ControlFlowNode for Subscript | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header |
8+
| flask_bad.py:27:18:27:24 | ControlFlowNode for request | flask_bad.py:27:18:27:29 | ControlFlowNode for Attribute |
9+
| flask_bad.py:27:18:27:29 | ControlFlowNode for Attribute | flask_bad.py:27:18:27:43 | ControlFlowNode for Subscript |
10+
| flask_bad.py:27:18:27:43 | ControlFlowNode for Subscript | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header |
11+
| flask_bad.py:35:18:35:24 | ControlFlowNode for request | flask_bad.py:35:18:35:29 | ControlFlowNode for Attribute |
12+
| flask_bad.py:35:18:35:29 | ControlFlowNode for Attribute | flask_bad.py:35:18:35:43 | ControlFlowNode for Subscript |
13+
| flask_bad.py:35:18:35:43 | ControlFlowNode for Subscript | flask_bad.py:38:9:38:34 | ControlFlowNode for Dict |
14+
| flask_bad.py:44:44:44:50 | ControlFlowNode for request | flask_bad.py:44:44:44:55 | ControlFlowNode for Attribute |
15+
| flask_bad.py:44:44:44:55 | ControlFlowNode for Attribute | flask_bad.py:44:44:44:69 | ControlFlowNode for Subscript |
16+
| flask_bad.py:44:44:44:69 | ControlFlowNode for Subscript | flask_bad.py:44:29:44:70 | ControlFlowNode for Dict |
17+
nodes
18+
| flask_bad.py:9:18:9:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
19+
| flask_bad.py:9:18:9:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
20+
| flask_bad.py:9:18:9:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
21+
| flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
22+
| flask_bad.py:19:18:19:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
23+
| flask_bad.py:19:18:19:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
24+
| flask_bad.py:19:18:19:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
25+
| flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
26+
| flask_bad.py:27:18:27:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
27+
| flask_bad.py:27:18:27:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
28+
| flask_bad.py:27:18:27:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
29+
| flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | semmle.label | ControlFlowNode for rfs_header |
30+
| flask_bad.py:35:18:35:24 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
31+
| flask_bad.py:35:18:35:29 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
32+
| flask_bad.py:35:18:35:43 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
33+
| flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
34+
| flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | semmle.label | ControlFlowNode for Dict |
35+
| flask_bad.py:44:44:44:50 | ControlFlowNode for request | semmle.label | ControlFlowNode for request |
36+
| flask_bad.py:44:44:44:55 | ControlFlowNode for Attribute | semmle.label | ControlFlowNode for Attribute |
37+
| flask_bad.py:44:44:44:69 | ControlFlowNode for Subscript | semmle.label | ControlFlowNode for Subscript |
38+
#select
39+
| flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | flask_bad.py:9:18:9:24 | ControlFlowNode for request | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | $@ HTTP header is constructed from a $@. | flask_bad.py:12:31:12:40 | ControlFlowNode for rfs_header | This | flask_bad.py:9:18:9:24 | ControlFlowNode for request | user-provided value |
40+
| flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | flask_bad.py:19:18:19:24 | ControlFlowNode for request | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | $@ HTTP header is constructed from a $@. | flask_bad.py:21:38:21:47 | ControlFlowNode for rfs_header | This | flask_bad.py:19:18:19:24 | ControlFlowNode for request | user-provided value |
41+
| flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | flask_bad.py:27:18:27:24 | ControlFlowNode for request | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | $@ HTTP header is constructed from a $@. | flask_bad.py:29:34:29:43 | ControlFlowNode for rfs_header | This | flask_bad.py:27:18:27:24 | ControlFlowNode for request | user-provided value |
42+
| flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | flask_bad.py:35:18:35:24 | ControlFlowNode for request | flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | $@ HTTP header is constructed from a $@. | flask_bad.py:38:9:38:34 | ControlFlowNode for Dict | This | flask_bad.py:35:18:35:24 | ControlFlowNode for request | user-provided value |
43+
| flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | flask_bad.py:44:44:44:50 | ControlFlowNode for request | flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | $@ HTTP header is constructed from a $@. | flask_bad.py:44:29:44:70 | ControlFlowNode for Dict | This | flask_bad.py:44:44:44:50 | ControlFlowNode for request | user-provided value |

0 commit comments

Comments
 (0)