Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,10 @@ ql/python/ql/src/Security/CWE-079/ReflectedXss.ql
ql/python/ql/src/Security/CWE-089/SqlInjection.ql
ql/python/ql/src/Security/CWE-090/LdapInjection.ql
ql/python/ql/src/Security/CWE-094/CodeInjection.ql
ql/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
ql/python/ql/src/Security/CWE-113/HeaderInjection.ql
ql/python/ql/src/Security/CWE-116/BadTagFilter.ql
ql/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
ql/python/ql/src/Security/CWE-209/StackTraceExposure.ql
ql/python/ql/src/Security/CWE-215/FlaskDebug.ql
ql/python/ql/src/Security/CWE-285/PamAuthorization.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -106,9 +106,11 @@ ql/python/ql/src/Security/CWE-079/ReflectedXss.ql
ql/python/ql/src/Security/CWE-089/SqlInjection.ql
ql/python/ql/src/Security/CWE-090/LdapInjection.ql
ql/python/ql/src/Security/CWE-094/CodeInjection.ql
ql/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
ql/python/ql/src/Security/CWE-113/HeaderInjection.ql
ql/python/ql/src/Security/CWE-116/BadTagFilter.ql
ql/python/ql/src/Security/CWE-117/LogInjection.ql
ql/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
ql/python/ql/src/Security/CWE-209/StackTraceExposure.ql
ql/python/ql/src/Security/CWE-215/FlaskDebug.ql
ql/python/ql/src/Security/CWE-285/PamAuthorization.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@ ql/python/ql/src/Security/CWE-079/ReflectedXss.ql
ql/python/ql/src/Security/CWE-089/SqlInjection.ql
ql/python/ql/src/Security/CWE-090/LdapInjection.ql
ql/python/ql/src/Security/CWE-094/CodeInjection.ql
ql/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
ql/python/ql/src/Security/CWE-113/HeaderInjection.ql
ql/python/ql/src/Security/CWE-116/BadTagFilter.ql
ql/python/ql/src/Security/CWE-117/LogInjection.ql
ql/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
ql/python/ql/src/Security/CWE-209/StackTraceExposure.ql
ql/python/ql/src/Security/CWE-215/FlaskDebug.ql
ql/python/ql/src/Security/CWE-285/PamAuthorization.ql
Expand Down
13 changes: 13 additions & 0 deletions python/ql/lib/semmle/python/Concepts.qll
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ private import semmle.python.dataflow.new.TaintTracking
private import semmle.python.Files
private import semmle.python.Frameworks
private import semmle.python.security.internal.EncryptionKeySizes
private import semmle.python.dataflow.new.SensitiveDataSources
private import codeql.threatmodels.ThreatModels
private import codeql.concepts.ConceptsShared

Expand Down Expand Up @@ -1290,6 +1291,18 @@ module Http {
*/
DataFlow::Node getValueArg() { result = super.getValueArg() }

/** Holds if the name of this cookie indicates it may contain sensitive information. */
predicate isSensitive() {
exists(DataFlow::Node name |
name = [this.getNameArg(), this.getHeaderArg()] and
(
DataFlow::localFlow(any(SensitiveDataSource src), name)
or
name = sensitiveLookupStringConst(_)
)
)
}

/**
* Holds if the `Secure` flag of the cookie is known to have a value of `b`.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -334,3 +334,5 @@ private module SensitiveDataModeling {
}

predicate sensitiveDataExtraStepForCalls = SensitiveDataModeling::extraStepForCalls/2;

predicate sensitiveLookupStringConst = SensitiveDataModeling::sensitiveLookupStringConst/1;
26 changes: 26 additions & 0 deletions python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>Cookies without the <code>HttpOnly</code> flag set are accessible to JavaScript running in the same origin.
In case of a Cross-Site Scripting (XSS) vulnerability, the cookie can be stolen by a malicious script.
If a sensitive cookie does not need to be accessed directly by client-side JS, the <code>HttpOnly</code> flag should be set.</p>
</overview>

<recommendation>
<p>Set <code>httponly</code> to <code>True</code>, or add <code>; HttpOnly;</code> to the cookie's raw header value, to ensure that the cookie is not accessible via JavaScript.</p>
</recommendation>

<example>
<p>In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the case marked BAD they are not set.</p>
<sample src="examples/InsecureCookie.py" />
</example>

<references>
<li>PortSwigger: <a href="https://portswigger.net/kb/issues/00500600_cookie-without-httponly-flag-set">Cookie without HttpOnly flag set</a></li>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie</a>.</li>
</references>

</qhelp>
21 changes: 21 additions & 0 deletions python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @name Sensitive cookie missing `HttpOnly` attribute.
* @description Cookies without the `HttpOnly` attribute set can be accessed by JS scripts, making them more vulnerable to XSS attacks.
* @kind problem
* @problem.severity warning
* @security-severity 5.0
* @precision high
* @id py/client-exposed-cookie
* @tags security
* external/cwe/cwe-1004
*/

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

from Http::Server::CookieWrite cookie
where
cookie.hasHttpOnlyFlag(false) and
cookie.isSensitive()
select cookie, "Sensitive server cookie is set without HttpOnly flag."
Comment on lines +17 to +21

Check warning

Code scanning / CodeQL

Consistent alert message Warning

The py/client-exposed-cookie query does not have the same alert message as js.
21 changes: 21 additions & 0 deletions python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from flask import Flask, request, make_response, Response


@app.route("/good1")
def good1():
resp = make_response()
resp.set_cookie("sessionid", value="value", secure=True, httponly=True, samesite='Strict') # GOOD: Attributes are securely set
return resp


@app.route("/good2")
def good2():
resp = make_response()
resp.headers['Set-Cookie'] = "sessionid=value; Secure; HttpOnly; SameSite=Strict" # GOOD: Attributes are securely set
return resp

@app.route("/bad1")
def bad1():
resp = make_response()
resp.set_cookie("sessionid", value="value", samesite='None') # BAD: the SameSite attribute is set to 'None' and the 'Secure' and 'HttpOnly' attributes are set to False by default.
return resp
26 changes: 26 additions & 0 deletions python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>Cookies with the <code>SameSite</code> attribute set to <code>'None'</code> will be sent with cross-origin requests.
This can sometimes allow for Cross-Site Request Forgery (CSRF) attacks, in which a third-party site could perform actions on behalf of a user, if the cookie is used for authentication.</p>
</overview>

<recommendation>
<p>Set the <code>samesite</code> to <code>Lax</code> or <code>Strict</code>, or add <code>; SameSite=Lax;</code>, or
<code>; SameSite=Strict;</code> to the cookie's raw header value. The default value in most cases is <code>Lax</code>.</p>
</recommendation>

<example>
<p>In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the case marked BAD they are not set.</p>
<sample src="examples/InsecureCookie.py" />
</example>

<references>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie</a>.</li>
<li>OWASP: <a href="https://owasp.org/www-community/SameSite">SameSite</a>.</li>
</references>

</qhelp>
21 changes: 21 additions & 0 deletions python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* @name Sensitive cookie with `SameSite` attribute set to `None`.
* @description Cookies with `SameSite` set to `None` can allow for Cross-Site Request Forgery (CSRF) attacks.
* @kind problem
* @problem.severity warning
* @security-severity 4.0
* @precision high
* @id py/samesite-none-cookie
* @tags security
* external/cwe/cwe-1275
*/

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

from Http::Server::CookieWrite cookie
where
cookie.hasSameSiteAttribute(any(Http::Server::CookieWrite::SameSiteNone v)) and
cookie.isSensitive()
select cookie, "Sensitive cookie with SameSite set to 'None'."
21 changes: 21 additions & 0 deletions python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from flask import Flask, request, make_response, Response


@app.route("/good1")
def good1():
resp = make_response()
resp.set_cookie("sessionid", value="value", secure=True, httponly=True, samesite='Strict') # GOOD: Attributes are securely set
return resp


@app.route("/good2")
def good2():
resp = make_response()
resp.headers['Set-Cookie'] = "sessionid=value; Secure; HttpOnly; SameSite=Strict" # GOOD: Attributes are securely set
return resp

@app.route("/bad1")
def bad1():
resp = make_response()
resp.set_cookie("sessionid", value="value", samesite='None') # BAD: the SameSite attribute is set to 'None' and the 'Secure' and 'HttpOnly' attributes are set to False by default.
return resp
15 changes: 7 additions & 8 deletions python/ql/src/Security/CWE-614/InsecureCookie.qhelp
Original file line number Diff line number Diff line change
Expand Up @@ -4,26 +4,25 @@
<qhelp>

<overview>
<p>Cookies without the <code>Secure</code> flag set may be transmitted using HTTP instead of HTTPS, which leaves them vulnerable to reading by a third party.</p>
<p>Cookies without the <code>HttpOnly</code> flag set are accessible to JavaScript running in the same origin. In case of a Cross-Site Scripting (XSS) vulnerability, the cookie can be stolen by a malicious script.</p>
<p>Cookies with the <code>SameSite</code> attribute set to <code>'None'</code> will be sent with cross-origin requests, which can be controlled by third-party JavaScript code and allow for Cross-Site Request Forgery (CSRF) attacks.</p>
<p>Cookies without the <code>Secure</code> flag set may be transmitted using HTTP instead of HTTPS.
This leaves them vulnerable to being read by a third party attacker. If a sensitive cookie such as a session
key is intercepted this way, it would allow the attacker to perform actions on a user's behalf.</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>
<p>Always set <code>secure</code> to <code>True</code>, or add <code>; Secure;</code> to the cookie's raw header value, to ensure SSL is used to transmit the cookie
with encryption.</p>
</recommendation>

<example>
<p>In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the cases marked BAD they are not set.</p>
<p>In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the case marked BAD they are not set.</p>
<sample src="examples/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>
<li>MDN: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie">Set-Cookie</a>.</li>
</references>

</qhelp>
37 changes: 4 additions & 33 deletions python/ql/src/Security/CWE-614/InsecureCookie.ql
Original file line number Diff line number Diff line change
Expand Up @@ -9,43 +9,14 @@
* @id py/insecure-cookie
* @tags security
* external/cwe/cwe-614
* external/cwe/cwe-1004
* external/cwe/cwe-1275
*/

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

predicate hasProblem(Http::Server::CookieWrite cookie, string alert, int idx) {
from Http::Server::CookieWrite cookie
where
cookie.hasSecureFlag(false) and
alert = "Secure" and
idx = 0
or
cookie.hasHttpOnlyFlag(false) and
alert = "HttpOnly" and
idx = 1
or
cookie.hasSameSiteAttribute(any(Http::Server::CookieWrite::SameSiteNone v)) and
alert = "SameSite" and
idx = 2
}

predicate hasAlert(Http::Server::CookieWrite cookie, string alert) {
exists(int numProblems | numProblems = strictcount(string p | hasProblem(cookie, p, _)) |
numProblems = 1 and
alert = any(string prob | hasProblem(cookie, prob, _)) + " attribute"
or
numProblems = 2 and
alert =
strictconcat(string prob, int idx | hasProblem(cookie, prob, idx) | prob, " and " order by idx)
+ " attributes"
or
numProblems = 3 and
alert = "Secure, HttpOnly, and SameSite attributes"
)
}

from Http::Server::CookieWrite cookie, string alert
where hasAlert(cookie, alert)
select cookie, "Cookie is added without the " + alert + " properly set."
cookie.isSensitive()
select cookie, "Cookie is added to response without the 'secure' flag being set."
7 changes: 4 additions & 3 deletions python/ql/src/Security/CWE-614/examples/InsecureCookie.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,18 @@
@app.route("/good1")
def good1():
resp = make_response()
resp.set_cookie("name", value="value", secure=True, httponly=True, samesite='Strict') # GOOD: Attributes are securely set
resp.set_cookie("sessionid", value="value", secure=True, httponly=True, samesite='Strict') # GOOD: Attributes are securely set
return resp


@app.route("/good2")
def good2():
resp = make_response()
resp.headers['Set-Cookie'] = "name=value; Secure; HttpOnly; SameSite=Strict" # GOOD: Attributes are securely set
resp.headers['Set-Cookie'] = "sessionid=value; Secure; HttpOnly; SameSite=Strict" # GOOD: Attributes are securely set
return resp

@app.route("/bad1")
def bad1():
resp = make_response()
resp.set_cookie("name", value="value", samesite='None') # BAD: the SameSite attribute is set to 'None' and the 'Secure' and 'HttpOnly' attributes are set to False by default.
resp.set_cookie("sessionid", value="value", samesite='None') # BAD: the SameSite attribute is set to 'None' and the 'Secure' and 'HttpOnly' attributes are set to False by default.
return resp
4 changes: 4 additions & 0 deletions python/ql/src/change-notes/2025-09-19-insecure-cookie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: minorAnalysis
---
* The `py/insecure-cookie` query has been split into multiple queries; with `py/insecure-cookie` checking for cases in which `Secure` flag is not set, `py/client-exposed-cookie` checking for cases in which the `HttpOnly` flag is not set, and the `py/samesite-none` query checking for cases in which the `SameSite` attribute is set to `None`. These queries also now only alert for cases in which the cookie is detected to contain sensitive data.
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
| test.py:8:5:8:38 | ControlFlowNode for Attribute() | Sensitive server cookie is set without HttpOnly flag. |
| test.py:9:5:9:51 | ControlFlowNode for Attribute() | Sensitive server cookie is set without HttpOnly flag. |
| test.py:11:5:11:57 | ControlFlowNode for Attribute() | Sensitive server cookie is set without HttpOnly flag. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: Security/CWE-1004/NonHttpOnlyCookie.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
from flask import Flask, request, make_response

app = Flask(__name__)

@app.route("/test")
def test():
resp = make_response()
resp.set_cookie("oauth", "value1") # $Alert[py/client-exposed-cookie]
resp.set_cookie("oauth", "value2", secure=True) # $Alert[py/client-exposed-cookie]
resp.set_cookie("oauth", "value2", httponly=True)
resp.set_cookie("oauth", "value2", samesite="Strict") # $Alert[py/client-exposed-cookie]
resp.set_cookie("oauth", "value2", httponly=True, samesite="None")
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
| test.py:10:5:10:60 | ControlFlowNode for Attribute() | Sensitive cookie with SameSite set to 'None'. |
| test.py:13:5:13:78 | ControlFlowNode for Attribute() | Sensitive cookie with SameSite set to 'None'. |
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
query: Security/CWE-1275/SameSiteNoneCookie.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
from flask import Flask, request, make_response

app = Flask(__name__)

@app.route("/test")
def test(oauth_cookie_name):
resp = make_response()
resp.set_cookie("password", "value1")
resp.set_cookie("authKey", "value2", samesite="Lax")
resp.set_cookie("session_id", "value2", samesite="None") # $Alert[py/samesite-none-cookie]
resp.set_cookie("oauth", "value2", secure=True, samesite="Strict")
resp.set_cookie("oauth", "value2", httponly=True, samesite="Strict")
resp.set_cookie(oauth_cookie_name, "value2", secure=True, samesite="None") # $Alert[py/samesite-none-cookie]
resp.set_cookie("not_sensitive", "value2", samesite="None")

Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
| test.py:10:5:10:37 | ControlFlowNode for Attribute() | Cookie is added without the Secure and HttpOnly attributes properly set. |
| test.py:11:5:11:50 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
| test.py:12:5:12:52 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
| test.py:13:5:13:56 | ControlFlowNode for Attribute() | Cookie is added without the Secure and HttpOnly attributes properly set. |
| test.py:14:5:14:53 | ControlFlowNode for Attribute() | Cookie is added without the Secure and HttpOnly attributes properly set. |
| test.py:15:5:15:54 | ControlFlowNode for Attribute() | Cookie is added without the Secure, HttpOnly, and SameSite attributes properly set. |
| test.py:16:5:16:69 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
| test.py:17:5:17:71 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
| test.py:18:5:18:67 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly and SameSite attributes properly set. |
| test.py:19:5:19:69 | ControlFlowNode for Attribute() | Cookie is added without the Secure and SameSite attributes properly set. |
| test.py:8:5:8:40 | ControlFlowNode for Attribute() | Cookie is added to response without the 'secure' flag being set. |
| test.py:10:5:10:57 | ControlFlowNode for Attribute() | Cookie is added to response without the 'secure' flag being set. |
| test.py:11:5:11:60 | ControlFlowNode for Attribute() | Cookie is added to response without the 'secure' flag being set. |
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
Security/CWE-614/InsecureCookie.ql
query: Security/CWE-614/InsecureCookie.ql
postprocess: utils/test/InlineExpectationsTestQuery.ql
Loading