Skip to content

Commit 46f309c

Browse files
authored
Merge pull request #6360 from jorgectf/jorgectf/python/insecure-cookie
Python: Add cookie security-related queries
2 parents 9a4d86e + cff950f commit 46f309c

File tree

19 files changed

+672
-4
lines changed

19 files changed

+672
-4
lines changed
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
from flask import request, make_response
2+
3+
4+
@app.route("/1")
5+
def true():
6+
resp = make_response()
7+
resp.set_cookie(request.args["name"],
8+
value=request.args["name"])
9+
return resp
10+
11+
12+
@app.route("/2")
13+
def flask_make_response():
14+
resp = make_response("hello")
15+
resp.headers['Set-Cookie'] = f"{request.args['name']}={request.args['name']};"
16+
return resp
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>Constructing cookies from user input may allow an attacker to perform a Cookie Poisoning attack.
8+
It is possible, however, to perform other parameter-like attacks through cookie poisoning techniques,
9+
such as SQL Injection, Directory Traversal, or Stealth Commanding, etc. Additionally,
10+
cookie injection may relate to attempts to perform Access of Administrative Interface.
11+
</p>
12+
</overview>
13+
14+
<recommendation>
15+
<p>Do not use raw user input to construct cookies.</p>
16+
</recommendation>
17+
18+
<example>
19+
<p>This example shows two ways of adding a cookie to a Flask response. The first way uses <code>set_cookie</code>'s
20+
and the second sets a cookie's raw value through a header, both using user-supplied input.</p>
21+
<sample src="CookieInjection.py" />
22+
</example>
23+
24+
<references>
25+
<li>Imperva: <a href="https://docs.imperva.com/bundle/on-premises-knowledgebase-reference-guide/page/cookie_injection.htm">Cookie injection</a>.</li>
26+
</references>
27+
28+
</qhelp>
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
/**
2+
* @name Construction of a cookie using user-supplied input.
3+
* @description Constructing cookies from user input may allow an attacker to perform a Cookie Poisoning attack.
4+
* @kind path-problem
5+
* @problem.severity error
6+
* @id py/cookie-injection
7+
* @tags security
8+
* external/cwe/cwe-614
9+
*/
10+
11+
// determine precision above
12+
import python
13+
import semmle.python.dataflow.new.DataFlow
14+
import experimental.semmle.python.Concepts
15+
import experimental.semmle.python.CookieHeader
16+
import experimental.semmle.python.security.injection.CookieInjection
17+
import DataFlow::PathGraph
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 its " + sink.getNode().(CookieSink).getFlag() + " flag is not properly set."
26+
else insecure = "."
27+
select sink.getNode(), source, sink, "Cookie is constructed from a $@" + insecure, source.getNode(),
28+
"user-supplied input"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from flask import Flask, request, make_response, Response
2+
3+
4+
@app.route("/1")
5+
def true():
6+
resp = make_response()
7+
resp.set_cookie("name", value="value", secure=True)
8+
return resp
9+
10+
11+
@app.route("/2")
12+
def flask_make_response():
13+
resp = make_response("hello")
14+
resp.headers['Set-Cookie'] = "name=value; Secure;"
15+
return resp
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
<!DOCTYPE qhelp PUBLIC
2+
"-//Semmle//qhelp//EN"
3+
"qhelp.dtd">
4+
<qhelp>
5+
6+
<overview>
7+
<p>Setting the 'secure' flag on a cookie to <code>False</code> can cause it to be sent in cleartext.
8+
Setting the 'httponly' flag on a cookie to <code>False</code> may allow attackers access it via JavaScript.
9+
Setting the 'samesite' flag on a cookie to <code>'None'</code> will make the cookie to be sent in third-party
10+
contexts which may be attacker-controlled.</p>
11+
</overview>
12+
13+
<recommendation>
14+
<p>Always set <code>secure</code> to <code>True</code> or add "; Secure;" to the cookie's raw value.</p>
15+
<p>Always set <code>httponly</code> to <code>True</code> or add "; HttpOnly;" to the cookie's raw value.</p>
16+
<p>Always set <code>samesite</code> to <code>Lax</code> or <code>Strict</code>, or add "; SameSite=Lax;", or
17+
"; Samesite=Strict;" to the cookie's raw header value.</p>
18+
</recommendation>
19+
20+
<example>
21+
<p>This example shows two ways of adding a cookie to a Flask response. The first way uses <code>set_cookie</code>'s
22+
secure flag and the second adds the secure flag in the cookie's raw value.</p>
23+
<sample src="InsecureCookie.py" />
24+
</example>
25+
26+
<references>
27+
<li>Detectify: <a href="https://support.detectify.com/support/solutions/articles/48001048982-cookie-lack-secure-flag">Cookie lack Secure flag</a>.</li>
28+
<li>PortSwigger: <a href="https://portswigger.net/kb/issues/00500200_tls-cookie-without-secure-flag-set">TLS cookie without secure flag set</a>.</li>
29+
</references>
30+
31+
</qhelp>
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
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+
* @security-severity 5.0
8+
* @precision ???
9+
* @id py/insecure-cookie
10+
* @tags security
11+
* external/cwe/cwe-614
12+
*/
13+
14+
// TODO: determine precision above
15+
import python
16+
import semmle.python.dataflow.new.DataFlow
17+
import experimental.semmle.python.Concepts
18+
import experimental.semmle.python.CookieHeader
19+
20+
from Cookie cookie, string alert
21+
where
22+
not cookie.isSecure() and
23+
alert = "secure"
24+
or
25+
not cookie.isHttpOnly() and
26+
alert = "httponly"
27+
or
28+
not cookie.isSameSite() and
29+
alert = "samesite"
30+
select cookie, "Cookie is added without the '" + alert + "' flag properly set."

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

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ private import semmle.python.dataflow.new.DataFlow
1313
private import semmle.python.dataflow.new.RemoteFlowSources
1414
private import semmle.python.dataflow.new.TaintTracking
1515
private import experimental.semmle.python.Frameworks
16+
private import semmle.python.Concepts
1617

1718
/** Provides classes for modeling copying file related APIs. */
1819
module CopyFile {
@@ -370,6 +371,55 @@ class HeaderDeclaration extends DataFlow::Node {
370371
DataFlow::Node getValueArg() { result = range.getValueArg() }
371372
}
372373

374+
/**
375+
* A data-flow node that sets a cookie in an HTTP response.
376+
*
377+
* Extend this class to refine existing API models. If you want to model new APIs,
378+
* extend `Cookie::Range` instead.
379+
*/
380+
class Cookie extends HTTP::Server::CookieWrite instanceof Cookie::Range {
381+
/**
382+
* Holds if this cookie is secure.
383+
*/
384+
predicate isSecure() { super.isSecure() }
385+
386+
/**
387+
* Holds if this cookie is HttpOnly.
388+
*/
389+
predicate isHttpOnly() { super.isHttpOnly() }
390+
391+
/**
392+
* Holds if the cookie is SameSite
393+
*/
394+
predicate isSameSite() { super.isSameSite() }
395+
}
396+
397+
/** Provides a class for modeling new cookie writes on HTTP responses. */
398+
module Cookie {
399+
/**
400+
* A data-flow node that sets a cookie in an HTTP response.
401+
*
402+
* Extend this class to model new APIs. If you want to refine existing API models,
403+
* extend `Cookie` instead.
404+
*/
405+
abstract class Range extends HTTP::Server::CookieWrite::Range {
406+
/**
407+
* Holds if this cookie is secure.
408+
*/
409+
abstract predicate isSecure();
410+
411+
/**
412+
* Holds if this cookie is HttpOnly.
413+
*/
414+
abstract predicate isHttpOnly();
415+
416+
/**
417+
* Holds if the cookie is SameSite.
418+
*/
419+
abstract predicate isSameSite();
420+
}
421+
}
422+
373423
/** Provides classes for modeling JWT encoding-related APIs. */
374424
module JwtEncoding {
375425
/**
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
/**
2+
* Temporary: provides a class to extend current cookies to header declarations
3+
*/
4+
5+
import python
6+
import semmle.python.dataflow.new.DataFlow
7+
import semmle.python.dataflow.new.TaintTracking
8+
import experimental.semmle.python.Concepts
9+
10+
/**
11+
* Gets a header setting a cookie.
12+
*
13+
* Given the following example:
14+
*
15+
* ```py
16+
* @app.route("/")
17+
* def flask_make_response():
18+
* resp = make_response("")
19+
* resp.headers['Set-Cookie'] = "name=value; Secure;"
20+
* return resp
21+
* ```
22+
*
23+
* * `this` would be `resp.headers['Set-Cookie'] = "name=value; Secure;"`.
24+
* * `isSecure()` predicate would succeed.
25+
* * `isHttpOnly()` predicate would fail.
26+
* * `isSameSite()` predicate would fail.
27+
* * `getName()` and `getValue()` results would be `"name=value; Secure;"`.
28+
*/
29+
class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
30+
CookieHeader() {
31+
this instanceof HeaderDeclaration and
32+
exists(StrConst str |
33+
str.getText() = "Set-Cookie" and
34+
DataFlow::exprNode(str)
35+
.(DataFlow::LocalSourceNode)
36+
.flowsTo(this.(HeaderDeclaration).getNameArg())
37+
)
38+
}
39+
40+
override predicate isSecure() {
41+
exists(StrConst str |
42+
str.getText().regexpMatch(".*; *Secure;.*") and
43+
DataFlow::exprNode(str)
44+
.(DataFlow::LocalSourceNode)
45+
.flowsTo(this.(HeaderDeclaration).getValueArg())
46+
)
47+
}
48+
49+
override predicate isHttpOnly() {
50+
exists(StrConst str |
51+
str.getText().regexpMatch(".*; *HttpOnly;.*") and
52+
DataFlow::exprNode(str)
53+
.(DataFlow::LocalSourceNode)
54+
.flowsTo(this.(HeaderDeclaration).getValueArg())
55+
)
56+
}
57+
58+
override predicate isSameSite() {
59+
exists(StrConst str |
60+
str.getText().regexpMatch(".*; *SameSite=(Strict|Lax);.*") and
61+
DataFlow::exprNode(str)
62+
.(DataFlow::LocalSourceNode)
63+
.flowsTo(this.(HeaderDeclaration).getValueArg())
64+
)
65+
}
66+
67+
override DataFlow::Node getNameArg() { result = this.(HeaderDeclaration).getValueArg() }
68+
69+
override DataFlow::Node getValueArg() { result = this.(HeaderDeclaration).getValueArg() }
70+
71+
override DataFlow::Node getHeaderArg() { none() }
72+
}

0 commit comments

Comments
 (0)