Skip to content

Python: Add cookie security-related queries #6360

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 33 commits into from
May 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
0aaa9c1
Merge remote-tracking branch 'origin/jorgectf/python/headerInjection'…
jorgectf Jul 25, 2021
8a3e4f1
Add tests and `.qlref`
jorgectf Jul 25, 2021
c8983be
Add query
jorgectf Jul 25, 2021
4f68a17
Write documentation and example
jorgectf Jul 25, 2021
66fdd53
Merge branch 'jorgectf/python/headerInjection' into jorgectf/python/i…
jorgectf Jul 25, 2021
6504429
Add `CookieWrite` concept
jorgectf Jul 25, 2021
9834659
Polish `CookieWrite`
jorgectf Jul 25, 2021
c8a7f48
Add `.expected`
jorgectf Jul 25, 2021
54ed25a
Change `False` and `None` scopes
jorgectf Jul 25, 2021
28ec8c9
Merge remote-tracking branch 'origin/main' into jorgectf/python/insec…
jorgectf Oct 27, 2021
48c3c3d
Broaden scope
jorgectf Oct 27, 2021
0f2b81e
Polish tests
jorgectf Oct 28, 2021
5dc1ad6
Polish `.ql`
jorgectf Oct 28, 2021
129edd6
Update `.expected`
jorgectf Oct 28, 2021
cf9e9f9
Add cookie injection query missing proper tests
jorgectf Oct 28, 2021
4cb78ac
Fix typo
jorgectf Nov 5, 2021
d7a7946
Improve tests
jorgectf Nov 5, 2021
b3258ce
Add `CookieInjection` sample and `.qhelp`
jorgectf Nov 5, 2021
cf47e8e
Fix endpoints' naming
jorgectf Nov 5, 2021
a420e6e
Add `CookieInjection.qlref`
jorgectf Nov 5, 2021
86aac7c
Add/Update `.expected` files.
jorgectf Nov 5, 2021
ed74bd6
Merge remote-tracking branch 'origin/main' into jorgectf/python/insec…
jorgectf Nov 5, 2021
83e3de1
Polish documentation.
jorgectf Nov 5, 2021
e7d649f
Make `Cookie` concept extend `HTTP::Server::CookieWrite`
jorgectf Nov 16, 2021
6ecb6d1
Adapt Django and Flask to their main modelings
jorgectf Nov 16, 2021
a4204cc
Avoid using `Str_` internal class
jorgectf Nov 16, 2021
840cded
Avoid using `Str_` in `CookieHeader`
jorgectf Nov 16, 2021
d127d21
Merge branch 'main' into jorgectf/python/insecure-cookie
RasmusWL May 11, 2022
84ad45c
Python: Fix Django import
RasmusWL May 11, 2022
a902d3d
Python: Add `security-severity` for `py/insecure-cookie`
RasmusWL May 11, 2022
27b99c5
Python: Add placeholder precision for `py/insecure-cookie`
RasmusWL May 11, 2022
fc8633c
Python: Fix select for `py/cookie-injection`
RasmusWL May 11, 2022
cff950f
Python: Fix select of `py/insecure-cookie`
RasmusWL May 11, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions python/ql/src/experimental/Security/CWE-614/CookieInjection.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
from flask import request, make_response


@app.route("/1")
def true():
resp = make_response()
resp.set_cookie(request.args["name"],
value=request.args["name"])
return resp


@app.route("/2")
def flask_make_response():
resp = make_response("hello")
resp.headers['Set-Cookie'] = f"{request.args['name']}={request.args['name']};"
return resp
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>Constructing cookies from user input may allow an attacker to perform a Cookie Poisoning attack.
It is possible, however, to perform other parameter-like attacks through cookie poisoning techniques,
such as SQL Injection, Directory Traversal, or Stealth Commanding, etc. Additionally,
cookie injection may relate to attempts to perform Access of Administrative Interface.
</p>
</overview>

<recommendation>
<p>Do not use raw user input to construct cookies.</p>
</recommendation>

<example>
<p>This example shows two ways of adding a cookie to a Flask response. The first way uses <code>set_cookie</code>'s
and the second sets a cookie's raw value through a header, both using user-supplied input.</p>
<sample src="CookieInjection.py" />
</example>

<references>
<li>Imperva: <a href="https://docs.imperva.com/bundle/on-premises-knowledgebase-reference-guide/page/cookie_injection.htm">Cookie injection</a>.</li>
</references>

</qhelp>
28 changes: 28 additions & 0 deletions python/ql/src/experimental/Security/CWE-614/CookieInjection.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/**
* @name Construction of a cookie using user-supplied input.
* @description Constructing cookies from user input may allow an attacker to perform a Cookie Poisoning attack.
* @kind path-problem
* @problem.severity error
* @id py/cookie-injection
* @tags security
* external/cwe/cwe-614
*/

// determine precision above
import python
import semmle.python.dataflow.new.DataFlow
import experimental.semmle.python.Concepts
import experimental.semmle.python.CookieHeader
import experimental.semmle.python.security.injection.CookieInjection
import DataFlow::PathGraph

from
CookieInjectionFlowConfig config, DataFlow::PathNode source, DataFlow::PathNode sink,
string insecure
where
config.hasFlowPath(source, sink) and
if exists(sink.getNode().(CookieSink))
then insecure = ",and its " + sink.getNode().(CookieSink).getFlag() + " flag is not properly set."
else insecure = "."
select sink.getNode(), source, sink, "Cookie is constructed from a $@" + insecure, source.getNode(),
"user-supplied input"
15 changes: 15 additions & 0 deletions python/ql/src/experimental/Security/CWE-614/InsecureCookie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from flask import Flask, request, make_response, Response


@app.route("/1")
def true():
resp = make_response()
resp.set_cookie("name", value="value", secure=True)
return resp


@app.route("/2")
def flask_make_response():
resp = make_response("hello")
resp.headers['Set-Cookie'] = "name=value; Secure;"
return resp
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>Setting the 'secure' flag on a cookie to <code>False</code> can cause it to be sent in cleartext.
Setting the 'httponly' flag on a cookie to <code>False</code> may allow attackers access it via JavaScript.
Setting the 'samesite' flag on a cookie to <code>'None'</code> will make the cookie to be sent in third-party
contexts which may be attacker-controlled.</p>
</overview>

<recommendation>
<p>Always set <code>secure</code> to <code>True</code> or add "; Secure;" to the cookie's raw value.</p>
<p>Always set <code>httponly</code> to <code>True</code> or add "; HttpOnly;" to the cookie's raw value.</p>
<p>Always set <code>samesite</code> to <code>Lax</code> or <code>Strict</code>, or add "; SameSite=Lax;", or
"; Samesite=Strict;" to the cookie's raw header value.</p>
</recommendation>

<example>
<p>This example shows two ways of adding a cookie to a Flask response. The first way uses <code>set_cookie</code>'s
secure flag and the second adds the secure flag in the cookie's raw value.</p>
<sample src="InsecureCookie.py" />
</example>

<references>
<li>Detectify: <a href="https://support.detectify.com/support/solutions/articles/48001048982-cookie-lack-secure-flag">Cookie lack Secure flag</a>.</li>
<li>PortSwigger: <a href="https://portswigger.net/kb/issues/00500200_tls-cookie-without-secure-flag-set">TLS cookie without secure flag set</a>.</li>
</references>

</qhelp>
30 changes: 30 additions & 0 deletions python/ql/src/experimental/Security/CWE-614/InsecureCookie.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/**
* @name Failure to use secure cookies
* @description Insecure cookies may be sent in cleartext, which makes them vulnerable to
* interception.
* @kind problem
* @problem.severity error
* @security-severity 5.0
* @precision ???
* @id py/insecure-cookie
* @tags security
* external/cwe/cwe-614
*/

// TODO: determine precision above
import python
import semmle.python.dataflow.new.DataFlow
import experimental.semmle.python.Concepts
import experimental.semmle.python.CookieHeader

from Cookie cookie, string alert
where
not cookie.isSecure() and
alert = "secure"
or
not cookie.isHttpOnly() and
alert = "httponly"
or
not cookie.isSameSite() and
alert = "samesite"
select cookie, "Cookie is added without the '" + alert + "' flag properly set."
50 changes: 50 additions & 0 deletions python/ql/src/experimental/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ private import semmle.python.dataflow.new.DataFlow
private import semmle.python.dataflow.new.RemoteFlowSources
private import semmle.python.dataflow.new.TaintTracking
private import experimental.semmle.python.Frameworks
private import semmle.python.Concepts

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

/**
* A data-flow node that sets a cookie in an HTTP response.
*
* Extend this class to refine existing API models. If you want to model new APIs,
* extend `Cookie::Range` instead.
*/
class Cookie extends HTTP::Server::CookieWrite instanceof Cookie::Range {
/**
* Holds if this cookie is secure.
*/
predicate isSecure() { super.isSecure() }

/**
* Holds if this cookie is HttpOnly.
*/
predicate isHttpOnly() { super.isHttpOnly() }

/**
* Holds if the cookie is SameSite
*/
predicate isSameSite() { super.isSameSite() }
}

/** Provides a class for modeling new cookie writes on HTTP responses. */
module Cookie {
/**
* A data-flow node that sets a cookie in an HTTP response.
*
* Extend this class to model new APIs. If you want to refine existing API models,
* extend `Cookie` instead.
*/
abstract class Range extends HTTP::Server::CookieWrite::Range {
/**
* Holds if this cookie is secure.
*/
abstract predicate isSecure();

/**
* Holds if this cookie is HttpOnly.
*/
abstract predicate isHttpOnly();

/**
* Holds if the cookie is SameSite.
*/
abstract predicate isSameSite();
}
}

/** Provides classes for modeling JWT encoding-related APIs. */
module JwtEncoding {
/**
Expand Down
72 changes: 72 additions & 0 deletions python/ql/src/experimental/semmle/python/CookieHeader.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* Temporary: provides a class to extend current cookies to header declarations
*/

import python
import semmle.python.dataflow.new.DataFlow
import semmle.python.dataflow.new.TaintTracking
import experimental.semmle.python.Concepts

/**
* Gets a header setting a cookie.
*
* Given the following example:
*
* ```py
* @app.route("/")
* def flask_make_response():
* resp = make_response("")
* resp.headers['Set-Cookie'] = "name=value; Secure;"
* return resp
* ```
*
* * `this` would be `resp.headers['Set-Cookie'] = "name=value; Secure;"`.
* * `isSecure()` predicate would succeed.
* * `isHttpOnly()` predicate would fail.
* * `isSameSite()` predicate would fail.
* * `getName()` and `getValue()` results would be `"name=value; Secure;"`.
*/
Comment on lines +10 to +28

Check warning

Code scanning / CodeQL

Class QLDoc style.

The QLDoc for a class should start with 'A', 'An', or 'The'.
class CookieHeader extends Cookie::Range instanceof HeaderDeclaration {
CookieHeader() {
this instanceof HeaderDeclaration and
exists(StrConst str |
str.getText() = "Set-Cookie" and
DataFlow::exprNode(str)
.(DataFlow::LocalSourceNode)
.flowsTo(this.(HeaderDeclaration).getNameArg())
)
}

override predicate isSecure() {
exists(StrConst str |
str.getText().regexpMatch(".*; *Secure;.*") and
DataFlow::exprNode(str)
.(DataFlow::LocalSourceNode)
.flowsTo(this.(HeaderDeclaration).getValueArg())
)
}

override predicate isHttpOnly() {
exists(StrConst str |
str.getText().regexpMatch(".*; *HttpOnly;.*") and
DataFlow::exprNode(str)
.(DataFlow::LocalSourceNode)
.flowsTo(this.(HeaderDeclaration).getValueArg())
)
}

override predicate isSameSite() {
exists(StrConst str |
str.getText().regexpMatch(".*; *SameSite=(Strict|Lax);.*") and
DataFlow::exprNode(str)
.(DataFlow::LocalSourceNode)
.flowsTo(this.(HeaderDeclaration).getValueArg())
)
}

override DataFlow::Node getNameArg() { result = this.(HeaderDeclaration).getValueArg() }

override DataFlow::Node getValueArg() { result = this.(HeaderDeclaration).getValueArg() }

override DataFlow::Node getHeaderArg() { none() }
}
Loading