Skip to content

Commit cf9e9f9

Browse files
committed
Add cookie injection query missing proper tests
1 parent 129edd6 commit cf9e9f9

7 files changed

Lines changed: 111 additions & 5 deletions

File tree

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @name Failure to use secure cookies
3+
* @description Insecure cookies may be sent in cleartext, which makes them vulnerable to
4+
* interception.
5+
* @kind problem
6+
* @problem.severity error
7+
* @id py/insecure-cookie
8+
* @tags security
9+
* external/cwe/cwe-614
10+
*/
11+
12+
// determine precision above
13+
import python
14+
import semmle.python.dataflow.new.DataFlow
15+
import experimental.semmle.python.Concepts
16+
import experimental.semmle.python.CookieHeader
17+
import experimental.semmle.python.security.injection.CookieInjection
18+
19+
from
20+
CookieInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink,
21+
string insecure
22+
where
23+
config.hasFlowPath(source, sink) and
24+
if exists(sink.getNode().(CookieSink))
25+
then insecure = "and it's " + sink.getNode().(CookieSink).getFlag() + " flag is not properly set"
26+
else insecure = ""
27+
select sink.getNode(), "Cookie is constructed from a", source.getNode(), "user-supplied input",
28+
insecure

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

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,16 @@ class Cookie extends DataFlow::Node {
322322
* Holds if the cookie is SameSite
323323
*/
324324
predicate isSameSite() { range.isSameSite() }
325+
326+
/**
327+
* Gets the argument containing the header name.
328+
*/
329+
DataFlow::Node getName() { result = range.getName() }
330+
331+
/**
332+
* Gets the argument containing the header value.
333+
*/
334+
DataFlow::Node getValue() { result = range.getValue() }
325335
}
326336

327337
/** Provides a class for modeling new cookie writes on HTTP responses. */
@@ -347,5 +357,15 @@ module Cookie {
347357
* Holds if the cookie is SameSite.
348358
*/
349359
abstract predicate isSameSite();
360+
361+
/**
362+
* Gets the argument containing the header name.
363+
*/
364+
abstract DataFlow::Node getName();
365+
366+
/**
367+
* Gets the argument containing the header value.
368+
*/
369+
abstract DataFlow::Node getValue();
350370
}
351371
}

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

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,28 @@ import experimental.semmle.python.Concepts
99

1010
class CookieHeader extends HeaderDeclaration, Cookie::Range {
1111
CookieHeader() {
12-
this instanceof HeaderDeclaration and this.getNameArg().asExpr().(Str_).getS() = "Set-Cookie"
12+
this instanceof HeaderDeclaration and
13+
this.(HeaderDeclaration).getNameArg().asExpr().(Str_).getS() = "Set-Cookie"
1314
}
1415

1516
override predicate isSecure() {
16-
this.getValueArg().asExpr().(Str_).getS().regexpMatch(".*; *Secure;.*")
17+
this.(HeaderDeclaration).getValueArg().asExpr().(Str_).getS().regexpMatch(".*; *Secure;.*")
1718
}
1819

1920
override predicate isHttpOnly() {
20-
this.getValueArg().asExpr().(Str_).getS().regexpMatch(".*; *HttpOnly;.*")
21+
this.(HeaderDeclaration).getValueArg().asExpr().(Str_).getS().regexpMatch(".*; *HttpOnly;.*")
2122
}
2223

2324
override predicate isSameSite() {
24-
this.getValueArg().asExpr().(Str_).getS().regexpMatch(".*; *SameSite=(Strict|Lax);.*")
25+
this.(HeaderDeclaration)
26+
.getValueArg()
27+
.asExpr()
28+
.(Str_)
29+
.getS()
30+
.regexpMatch(".*; *SameSite=(Strict|Lax);.*")
2531
}
32+
33+
override DataFlow::Node getName() { result = this.(HeaderDeclaration).getValueArg() }
34+
35+
override DataFlow::Node getValue() { result = this.(HeaderDeclaration).getValueArg() }
2636
}

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,10 @@ private module PrivateDjango {
9090
class DjangoSetCookieCall extends DataFlow::CallCfgNode, Cookie::Range {
9191
DjangoSetCookieCall() { this = baseClassRef().getMember("set_cookie").getACall() }
9292

93+
override DataFlow::Node getName() { result = this.getArg(0) }
94+
95+
override DataFlow::Node getValue() { result = this.getArgByName("value") }
96+
9397
override predicate isSecure() {
9498
DataFlow::exprNode(any(True t))
9599
.(DataFlow::LocalSourceNode)

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,10 @@ module ExperimentalFlask {
9191
.getACall()
9292
}
9393

94+
override DataFlow::Node getName() { result = this.getArg(0) }
95+
96+
override DataFlow::Node getValue() { result = this.getArgByName("value") }
97+
9498
override predicate isSecure() {
9599
DataFlow::exprNode(any(True t))
96100
.(DataFlow::LocalSourceNode)
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
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+
class CookieSink extends DataFlow::Node {
8+
string flag;
9+
10+
CookieSink() {
11+
exists(Cookie cookie |
12+
this in [cookie.getName(), cookie.getValue()] and
13+
(
14+
not cookie.isSecure() and
15+
flag = "secure"
16+
or
17+
not cookie.isHttpOnly() and
18+
flag = "httponly"
19+
or
20+
not cookie.isSameSite() and
21+
flag = "samesite"
22+
)
23+
)
24+
}
25+
26+
string getFlag() { result = flag }
27+
}
28+
29+
/**
30+
* A taint-tracking configuration for detecting Cookie injections.
31+
*/
32+
class CookieInjectionFlowConfig extends TaintTracking::Configuration {
33+
CookieInjectionFlowConfig() { this = "CookieInjectionFlowConfig" }
34+
35+
override predicate isSource(DataFlow::Node source) { source instanceof RemoteFlowSource }
36+
37+
override predicate isSink(DataFlow::Node sink) {
38+
exists(Cookie c | sink in [c.getName(), c.getValue()])
39+
}
40+
}

python/ql/test/experimental/query-tests/Security/CWE-614/flask_bad.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
@app.route("/false")
77
def false():
88
resp = make_response()
9-
resp.set_cookie("name", value="value", secure=False,
9+
resp.set_cookie(request.args["name"], value=request.args["value"], secure=False,
1010
httponly=False, samesite='None')
1111
return resp
1212

0 commit comments

Comments
 (0)