Skip to content

Commit 68d9091

Browse files
Add to header write concept a specification of whether the name or value arg allows newlines.
Ported sink defenitions from Flask and Werzeug from experimental to main. Removed experimental sink definitions for Django, as neither name nor value are vulnerable.
1 parent 25ffcb2 commit 68d9091

File tree

8 files changed

+77
-87
lines changed

8 files changed

+77
-87
lines changed

python/ql/lib/semmle/python/Concepts.qll

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1041,6 +1041,16 @@ module Http {
10411041
* Gets the argument containing the header value.
10421042
*/
10431043
DataFlow::Node getValueArg() { result = super.getValueArg() }
1044+
1045+
/**
1046+
* Holds if newlines are accepted in the header name argument.
1047+
*/
1048+
predicate nameAllowsNewline() { super.nameAllowsNewline() }
1049+
1050+
/**
1051+
* Holds if newlines are accepted in the header value argument.
1052+
*/
1053+
predicate valueAllowsNewline() { super.valueAllowsNewline() }
10441054
}
10451055

10461056
/** Provides a class for modelling header writes on HTTP responses. */
@@ -1061,6 +1071,16 @@ module Http {
10611071
* Gets the argument containing the header value.
10621072
*/
10631073
abstract DataFlow::Node getValueArg();
1074+
1075+
/**
1076+
* Holds if newlines are accepted in the header name argument.
1077+
*/
1078+
abstract predicate nameAllowsNewline();
1079+
1080+
/**
1081+
* Holds if newlines are accepted in the header value argument.
1082+
*/
1083+
abstract predicate valueAllowsNewline();
10641084
}
10651085
}
10661086

python/ql/lib/semmle/python/frameworks/Flask.qll

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -220,6 +220,13 @@ module Flask {
220220

221221
/** Gets a reference to an instance of `flask.Response`. */
222222
DataFlow::Node instance() { instance(DataFlow::TypeTracker::end()).flowsTo(result) }
223+
224+
/** An `Headers` instance that is part of a Flask response. */
225+
private class FlaskResponseHeadersInstances extends Werkzeug::Headers::InstanceSource {
226+
FlaskResponseHeadersInstances() { this = request().getMember("headers").asSource() }
227+
}
228+
229+
// TODO: headers arg to make_response
223230
}
224231

225232
// ---------------------------------------------------------------------------

python/ql/lib/semmle/python/frameworks/Werkzeug.qll

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -182,8 +182,52 @@ module Werkzeug {
182182

183183
override string getAsyncMethodName() { none() }
184184
}
185+
186+
/** A call to a method that writes to a header, assumed to be a response header. */
187+
private class HeaderWriteCall extends Http::Server::ResponseHeaderWrite::Range,
188+
DataFlow::MethodCallNode
189+
{
190+
HeaderWriteCall() {
191+
this.getObject() = instance() and
192+
this.getMethodName() = ["add", "add_header", "set", "set_default", "__setitem__"]
193+
}
194+
195+
override DataFlow::Node getNameArg() { result = this.getArg(0) }
196+
197+
override DataFlow::Node getValueArg() { result = this.getArg(1) }
198+
199+
override predicate nameAllowsNewline() { any() }
200+
201+
override predicate valueAllowsNewline() { none() }
202+
}
203+
204+
/** A dict-like write to a header, assumed to be a response header. */
205+
private class HeaderWriteSubscript extends Http::Server::ResponseHeaderWrite::Range,
206+
DataFlow::Node
207+
{
208+
DataFlow::Node name;
209+
DataFlow::Node value;
210+
211+
HeaderWriteSubscript() {
212+
exists(SubscriptNode subscript |
213+
this.asCfgNode() = subscript and
214+
value.asCfgNode() = subscript.(DefinitionNode).getValue() and
215+
name.asCfgNode() = subscript.getIndex() and
216+
subscript.getObject() = instance().asCfgNode()
217+
)
218+
}
219+
220+
override DataFlow::Node getNameArg() { result = name }
221+
222+
override DataFlow::Node getValueArg() { result = value }
223+
224+
override predicate nameAllowsNewline() { any() }
225+
226+
override predicate valueAllowsNewline() { none() }
227+
}
185228
}
186229

230+
// TODO: `extend` bulk header update
187231
/**
188232
* Provides models for the `werkzeug.datastructures.Authorization` class
189233
*

python/ql/lib/semmle/python/security/dataflow/HttpHeaderInjectionCustomizations.qll

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ module HttpHeaderInjection {
4141
*/
4242
class HeaderWriteAsSink extends Sink {
4343
HeaderWriteAsSink() {
44-
exists(Http::Server::ResponseHeaderWrite headerDeclaration |
45-
this in [headerDeclaration.getNameArg(), headerDeclaration.getValueArg()]
44+
exists(Http::Server::ResponseHeaderWrite headerWrite |
45+
headerWrite.nameAllowsNewline() and
46+
this = headerWrite.getNameArg()
47+
or
48+
headerWrite.valueAllowsNewline() and
49+
this = headerWrite.getValueArg()
4650
)
4751
}
4852
}

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

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
private import experimental.semmle.python.frameworks.Stdlib
66
private import experimental.semmle.python.frameworks.Flask
77
private import experimental.semmle.python.frameworks.Django
8-
private import experimental.semmle.python.frameworks.Werkzeug
98
private import experimental.semmle.python.frameworks.LDAP
109
private import experimental.semmle.python.frameworks.JWT
1110
private import experimental.semmle.python.frameworks.Csv

python/ql/src/experimental/semmle/python/frameworks/Django.qll

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -88,31 +88,6 @@ private module ExperimentalPrivateDjango {
8888
result = baseClassRef().getReturn().getAMember()
8989
}
9090

91-
class DjangoResponseSetItemCall extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
92-
DjangoResponseSetItemCall() {
93-
this = baseClassRef().getReturn().getMember("__setitem__").getACall()
94-
}
95-
96-
override DataFlow::Node getNameArg() { result = this.getArg(0) }
97-
98-
override DataFlow::Node getValueArg() { result = this.getArg(1) }
99-
}
100-
101-
class DjangoResponseDefinition extends DataFlow::Node, HeaderDeclaration::Range {
102-
DataFlow::Node headerInput;
103-
104-
DjangoResponseDefinition() {
105-
headerInput = headerInstance().asSink() and
106-
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
107-
}
108-
109-
override DataFlow::Node getNameArg() {
110-
result.asExpr() = this.asExpr().(Subscript).getIndex()
111-
}
112-
113-
override DataFlow::Node getValueArg() { result = headerInput }
114-
}
115-
11691
/**
11792
* Gets a call to `set_cookie()`.
11893
*

python/ql/src/experimental/semmle/python/frameworks/Flask.qll

Lines changed: 0 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -42,32 +42,6 @@ module ExperimentalFlask {
4242
headerInstance().asExpr() = result.asExpr().(Subscript).getObject()
4343
}
4444

45-
class FlaskHeaderDefinition extends DataFlow::Node, HeaderDeclaration::Range {
46-
DataFlow::Node headerInput;
47-
48-
FlaskHeaderDefinition() {
49-
this.asCfgNode().(DefinitionNode) = headerInstanceCall().asCfgNode() and
50-
headerInput.asCfgNode() = this.asCfgNode().(DefinitionNode).getValue()
51-
}
52-
53-
override DataFlow::Node getNameArg() { result.asExpr() = this.asExpr().(Subscript).getIndex() }
54-
55-
override DataFlow::Node getValueArg() { result = headerInput }
56-
}
57-
58-
private class FlaskMakeResponseExtend extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
59-
KeyValuePair item;
60-
61-
FlaskMakeResponseExtend() {
62-
this.getFunction() = headerInstanceCall() and
63-
item = this.getArg(_).asExpr().(Dict).getAnItem()
64-
}
65-
66-
override DataFlow::Node getNameArg() { result.asExpr() = item.getKey() }
67-
68-
override DataFlow::Node getValueArg() { result.asExpr() = item.getValue() }
69-
}
70-
7145
private class FlaskResponse extends DataFlow::CallCfgNode, HeaderDeclaration::Range {
7246
KeyValuePair item;
7347

python/ql/src/experimental/semmle/python/frameworks/Werkzeug.qll

Lines changed: 0 additions & 33 deletions
This file was deleted.

0 commit comments

Comments
 (0)