From 7eabed6594c195cbb10b416f8c0dd636e71fb2e6 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 18 Sep 2025 13:34:10 +0100
Subject: [PATCH 01/13] Split insecure cookies queries into 3 queries
---
.../Security/CWE-1004/NonHttpOnlyCookie.ql | 19 ++++++++++
.../Security/CWE-1275/SameSiteNoneCookie.ql | 19 ++++++++++
.../ql/src/Security/CWE-614/InsecureCookie.ql | 37 ++-----------------
3 files changed, 41 insertions(+), 34 deletions(-)
create mode 100644 python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
create mode 100644 python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
diff --git a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
new file mode 100644
index 000000000000..fbd0ce3832c3
--- /dev/null
+++ b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
@@ -0,0 +1,19 @@
+/**
+ * @name 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)
+select cookie, "Cookie is added without the HttpOnly attribute properly set."
diff --git a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
new file mode 100644
index 000000000000..67886a204b69
--- /dev/null
+++ b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
@@ -0,0 +1,19 @@
+/**
+ * @name 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 5.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))
+select cookie, "Cookie is added with the SameSite attribute set to None."
diff --git a/python/ql/src/Security/CWE-614/InsecureCookie.ql b/python/ql/src/Security/CWE-614/InsecureCookie.ql
index 260bd303310d..bf6c5ce44d07 100644
--- a/python/ql/src/Security/CWE-614/InsecureCookie.ql
+++ b/python/ql/src/Security/CWE-614/InsecureCookie.ql
@@ -9,43 +9,12 @@
* @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) {
- 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."
+from Http::Server::CookieWrite cookie
+where cookie.hasSecureFlag(false)
+select cookie, "Cookie is added without the Secure attribute properly set."
From 04316d306fc4bc47a0ac32faddafb11077d57074 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 19 Sep 2025 12:42:30 +0100
Subject: [PATCH 02/13] Update qhelp
---
.../Security/CWE-1004/NotHttpOnlyCookie.qhelp | 26 +++++++++++++++++++
.../CWE-1004/examples/InsecureCookie.py | 21 +++++++++++++++
.../CWE-1275/SameSiteNoneCookie.qhelp | 26 +++++++++++++++++++
.../CWE-1275/examples/InsecureCookie.py | 21 +++++++++++++++
.../src/Security/CWE-614/InsecureCookie.qhelp | 15 +++++------
.../CWE-614/examples/InsecureCookie.py | 1 +
6 files changed, 102 insertions(+), 8 deletions(-)
create mode 100644 python/ql/src/Security/CWE-1004/NotHttpOnlyCookie.qhelp
create mode 100644 python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
create mode 100644 python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
create mode 100644 python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
diff --git a/python/ql/src/Security/CWE-1004/NotHttpOnlyCookie.qhelp b/python/ql/src/Security/CWE-1004/NotHttpOnlyCookie.qhelp
new file mode 100644
index 000000000000..01c472021ad0
--- /dev/null
+++ b/python/ql/src/Security/CWE-1004/NotHttpOnlyCookie.qhelp
@@ -0,0 +1,26 @@
+
+
+
+
+Cookies without the HttpOnly 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 cookie does not need to be accessed directly by client-side JS, the HttpOnly flag should be set.
+
+
+
+Set httponly to True, or add ; HttpOnly; to the cookie's raw header value, to ensure that the cookie is not accessible via JavaScript.
+
+
+
+In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the case marked BAD they are not set.
+
+
+
+
+PortSwigger: Cookie without HttpOnly flag set
+MDN: Set-Cookie.
+
+
+
diff --git a/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py b/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
new file mode 100644
index 000000000000..8ca12936a120
--- /dev/null
+++ b/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
@@ -0,0 +1,21 @@
+from flask import Flask, request, make_response, Response
+
+
+@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
+ 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
+ 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.
+ return resp
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
new file mode 100644
index 000000000000..e38ef00433a4
--- /dev/null
+++ b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
@@ -0,0 +1,26 @@
+
+
+
+
+Cookies with the SameSite attribute set to 'None' 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.
+
+
+
+Set the samesite to Lax or Strict, or add ; SameSite=Lax;, or
+; SameSite=Strict; to the cookie's raw header value. The default value in most cases is Lax.
+
+
+
+In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the case marked BAD they are not set.
+
+
+
+
+MDN: Set-Cookie.
+OWASP: SameSite.
+
+
+
diff --git a/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py b/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
new file mode 100644
index 000000000000..8ca12936a120
--- /dev/null
+++ b/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
@@ -0,0 +1,21 @@
+from flask import Flask, request, make_response, Response
+
+
+@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
+ 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
+ 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.
+ return resp
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-614/InsecureCookie.qhelp b/python/ql/src/Security/CWE-614/InsecureCookie.qhelp
index 5b36c9cc59d3..914d9d0baa58 100644
--- a/python/ql/src/Security/CWE-614/InsecureCookie.qhelp
+++ b/python/ql/src/Security/CWE-614/InsecureCookie.qhelp
@@ -4,26 +4,25 @@
-Cookies without the Secure flag set may be transmitted using HTTP instead of HTTPS, which leaves them vulnerable to reading by a third party.
-Cookies without the HttpOnly 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.
-Cookies with the SameSite attribute set to 'None' 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.
+Cookies without the Secure 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.
-Always set secure to True or add "; Secure;" to the cookie's raw value.
-Always set httponly to True or add "; HttpOnly;" to the cookie's raw value.
-Always set samesite to Lax or Strict, or add "; SameSite=Lax;", or
-"; Samesite=Strict;" to the cookie's raw header value.
+Always set secure to True, or add ; Secure; to the cookie's raw header value, to ensure SSL is used to transmit the cookie
+with encryption.
-In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the cases marked BAD they are not set.
+In the following examples, the cases marked GOOD show secure cookie attributes being set; whereas in the case marked BAD they are not set.
Detectify: Cookie lack Secure flag.
PortSwigger: TLS cookie without secure flag set.
+MDN: Set-Cookie.
diff --git a/python/ql/src/Security/CWE-614/examples/InsecureCookie.py b/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
index 07cca6c3fcee..8ca12936a120 100644
--- a/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
+++ b/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
@@ -15,6 +15,7 @@ def good2():
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.
return resp
\ No newline at end of file
From 2e95c2b3c2f23cdad0a8d13fd60396053acb03a1 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 19 Sep 2025 14:41:02 +0100
Subject: [PATCH 03/13] Split test cases for insecure cookie queries
---
.../Security/CWE-1275/SameSiteNoneCookie.ql | 2 +-
.../NonHttpOnlyCookie.expected | 7 +++++++
.../NonHttpOnlyCookie.qlref | 2 ++
.../CWE-1004-NonHttpOnlyCookie/test.py | 18 ++++++++++++++++++
.../SameSiteNoneCookie.expected | 3 +++
.../SameSiteNoneCookie.qlref | 2 ++
.../CWE-1275-SameSiteNoneCookie/test.py | 18 ++++++++++++++++++
.../InsecureCookie.expected | 17 +++++++----------
.../InsecureCookie.qlref | 3 ++-
.../Security/CWE-614-InsecureCookie/test.py | 18 ++++++++----------
10 files changed, 68 insertions(+), 22 deletions(-)
create mode 100644 python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
create mode 100644 python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.qlref
create mode 100644 python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
create mode 100644 python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
create mode 100644 python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.qlref
create mode 100644 python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
diff --git a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
index 67886a204b69..58f0e358390f 100644
--- a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
+++ b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
@@ -3,7 +3,7 @@
* @description Cookies with `SameSite` set to `None` can allow for Cross-Site Request Forgery (CSRF) attacks.
* @kind problem
* @problem.severity warning
- * @security-severity 5.0
+ * @security-severity 3.5
* @precision high
* @id py/samesite-none-cookie
* @tags security
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
new file mode 100644
index 000000000000..fc1ceee53d56
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
@@ -0,0 +1,7 @@
+| test.py:8:5:8:37 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:9:5:9:50 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:11:5:11:56 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:12:5:12:53 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:13:5:13:54 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:14:5:14:69 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:16:5:16:67 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.qlref b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.qlref
new file mode 100644
index 000000000000..7173918e3608
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.qlref
@@ -0,0 +1,2 @@
+query: Security/CWE-1004/NonHttpOnlyCookie.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
new file mode 100644
index 000000000000..b036cc0e45ae
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
@@ -0,0 +1,18 @@
+from flask import Flask, request, make_response
+
+app = Flask(__name__)
+
+@app.route("/test")
+def test():
+ resp = make_response()
+ resp.set_cookie("key1", "value1") # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", secure=True) # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", httponly=True)
+ resp.set_cookie("key2", "value2", samesite="Strict") # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", samesite="Lax") # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", samesite="None") # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", secure=True, samesite="Strict") # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", httponly=True, samesite="Strict")
+ resp.set_cookie("key2", "value2", secure=True, samesite="None") # $Alert[py/client-exposed-cookie]
+ resp.set_cookie("key2", "value2", httponly=True, samesite="None")
+ resp.set_cookie("key2", "value2", secure=True, httponly=True, samesite="Strict")
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
new file mode 100644
index 000000000000..71a578b328d5
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
@@ -0,0 +1,3 @@
+| test.py:13:5:13:54 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
+| test.py:16:5:16:67 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
+| test.py:17:5:17:69 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
diff --git a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.qlref b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.qlref
new file mode 100644
index 000000000000..ad4e08218b30
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.qlref
@@ -0,0 +1,2 @@
+query: Security/CWE-1275/SameSiteNoneCookie.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
new file mode 100644
index 000000000000..460594c1ab28
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
@@ -0,0 +1,18 @@
+from flask import Flask, request, make_response
+
+app = Flask(__name__)
+
+@app.route("/test")
+def test():
+ resp = make_response()
+ resp.set_cookie("key1", "value1")
+ resp.set_cookie("key2", "value2", secure=True)
+ resp.set_cookie("key2", "value2", httponly=True)
+ resp.set_cookie("key2", "value2", samesite="Strict")
+ resp.set_cookie("key2", "value2", samesite="Lax")
+ resp.set_cookie("key2", "value2", samesite="None") # $Alert[py/samesite-none-cookie]
+ resp.set_cookie("key2", "value2", secure=True, samesite="Strict")
+ resp.set_cookie("key2", "value2", httponly=True, samesite="Strict")
+ resp.set_cookie("key2", "value2", secure=True, samesite="None") # $Alert[py/samesite-none-cookie]
+ resp.set_cookie("key2", "value2", httponly=True, samesite="None") # $Alert[py/samesite-none-cookie]
+ resp.set_cookie("key2", "value2", secure=True, httponly=True, samesite="Strict")
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
index 95cad5b954e7..582a47589e6e 100644
--- a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
@@ -1,10 +1,7 @@
-| 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:37 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:10:5:10:52 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:11:5:11:56 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:12:5:12:53 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:13:5:13:54 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:15:5:15:71 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:17:5:17:69 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
diff --git a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.qlref b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.qlref
index d5143f6eaae1..f70206677198 100644
--- a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.qlref
+++ b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.qlref
@@ -1 +1,2 @@
-Security/CWE-614/InsecureCookie.ql
\ No newline at end of file
+query: Security/CWE-614/InsecureCookie.ql
+postprocess: utils/test/InlineExpectationsTestQuery.ql
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py
index 1a108fe41d49..9a305f0499f7 100644
--- a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py
+++ b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py
@@ -1,20 +1,18 @@
from flask import Flask, request, make_response
-import lxml.etree
-import markupsafe
app = Flask(__name__)
@app.route("/test")
def test():
resp = make_response()
- resp.set_cookie("key1", "value1")
- resp.set_cookie("key2", "value2", secure=True)
- resp.set_cookie("key2", "value2", httponly=True)
- resp.set_cookie("key2", "value2", samesite="Strict")
- resp.set_cookie("key2", "value2", samesite="Lax")
- resp.set_cookie("key2", "value2", samesite="None")
+ resp.set_cookie("key1", "value1") # $Alert[py/insecure-cookie]
+ resp.set_cookie("key2", "value2", secure=True)
+ resp.set_cookie("key2", "value2", httponly=True) # $Alert[py/insecure-cookie]
+ resp.set_cookie("key2", "value2", samesite="Strict") # $Alert[py/insecure-cookie]
+ resp.set_cookie("key2", "value2", samesite="Lax") # $Alert[py/insecure-cookie]
+ resp.set_cookie("key2", "value2", samesite="None") # $Alert[py/insecure-cookie]
resp.set_cookie("key2", "value2", secure=True, samesite="Strict")
- resp.set_cookie("key2", "value2", httponly=True, samesite="Strict")
+ resp.set_cookie("key2", "value2", httponly=True, samesite="Strict") # $Alert[py/insecure-cookie]
resp.set_cookie("key2", "value2", secure=True, samesite="None")
- resp.set_cookie("key2", "value2", httponly=True, samesite="None")
+ resp.set_cookie("key2", "value2", httponly=True, samesite="None") # $Alert[py/insecure-cookie]
resp.set_cookie("key2", "value2", secure=True, httponly=True, samesite="Strict")
\ No newline at end of file
From a9a258e7437f66923801869d7e1fac8678b97a48 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 19 Sep 2025 15:11:02 +0100
Subject: [PATCH 04/13] Add changenote
---
python/ql/src/change-notes/2025-09-19-insecure-cookie.md | 4 ++++
1 file changed, 4 insertions(+)
create mode 100644 python/ql/src/change-notes/2025-09-19-insecure-cookie.md
diff --git a/python/ql/src/change-notes/2025-09-19-insecure-cookie.md b/python/ql/src/change-notes/2025-09-19-insecure-cookie.md
new file mode 100644
index 000000000000..58415584a96b
--- /dev/null
+++ b/python/ql/src/change-notes/2025-09-19-insecure-cookie.md
@@ -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`.
\ No newline at end of file
From 6eac6b725863fa4716ec38e171c21d1b6abf651e Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Fri, 19 Sep 2025 17:03:19 +0100
Subject: [PATCH 05/13] Rename qhelp file
---
.../CWE-1004/{NotHttpOnlyCookie.qhelp => NonHttpOnlyCookie.qhelp} | 0
1 file changed, 0 insertions(+), 0 deletions(-)
rename python/ql/src/Security/CWE-1004/{NotHttpOnlyCookie.qhelp => NonHttpOnlyCookie.qhelp} (100%)
diff --git a/python/ql/src/Security/CWE-1004/NotHttpOnlyCookie.qhelp b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp
similarity index 100%
rename from python/ql/src/Security/CWE-1004/NotHttpOnlyCookie.qhelp
rename to python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp
From d28e8004fdb5b58e48f5a18dcd657229e71e12d8 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 23 Sep 2025 10:08:08 +0100
Subject: [PATCH 06/13] Add sensitive data heuristic
---
python/ql/lib/semmle/python/Concepts.qll | 13 +++++++++++++
.../python/dataflow/new/SensitiveDataSources.qll | 2 ++
python/ql/src/Security/CWE-614/InsecureCookie.ql | 3 ++-
.../InsecureCookie.expected | 10 +++-------
.../Security/CWE-614-InsecureCookie/test.py | 16 +++++-----------
5 files changed, 25 insertions(+), 19 deletions(-)
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index 16524aaf1dbd..0a6a43762476 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -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
@@ -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
+ (
+ name instanceof SensitiveDataSource
+ or
+ name = sensitiveLookupStringConst(_)
+ )
+ )
+ }
+
/**
* Holds if the `Secure` flag of the cookie is known to have a value of `b`.
*/
diff --git a/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll b/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll
index 0e017c4a2295..1a32965d08d7 100644
--- a/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll
+++ b/python/ql/lib/semmle/python/dataflow/new/SensitiveDataSources.qll
@@ -334,3 +334,5 @@ private module SensitiveDataModeling {
}
predicate sensitiveDataExtraStepForCalls = SensitiveDataModeling::extraStepForCalls/2;
+
+predicate sensitiveLookupStringConst = SensitiveDataModeling::sensitiveLookupStringConst/1;
diff --git a/python/ql/src/Security/CWE-614/InsecureCookie.ql b/python/ql/src/Security/CWE-614/InsecureCookie.ql
index bf6c5ce44d07..98e90c687caa 100644
--- a/python/ql/src/Security/CWE-614/InsecureCookie.ql
+++ b/python/ql/src/Security/CWE-614/InsecureCookie.ql
@@ -16,5 +16,6 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.Concepts
from Http::Server::CookieWrite cookie
-where cookie.hasSecureFlag(false)
+where cookie.hasSecureFlag(false) //and
+//cookie.isSensitive()
select cookie, "Cookie is added without the Secure attribute properly set."
diff --git a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
index 582a47589e6e..8d33578e5330 100644
--- a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
@@ -1,7 +1,3 @@
-| test.py:8:5:8:37 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:10:5:10:52 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:11:5:11:56 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:12:5:12:53 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:13:5:13:54 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:15:5:15:71 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:17:5:17:69 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:8:5:8:40 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:10:5:10:57 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
+| test.py:11:5:11:60 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
diff --git a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py
index 9a305f0499f7..214567275e5f 100644
--- a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py
+++ b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/test.py
@@ -5,14 +5,8 @@
@app.route("/test")
def test():
resp = make_response()
- resp.set_cookie("key1", "value1") # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", secure=True)
- resp.set_cookie("key2", "value2", httponly=True) # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", samesite="Strict") # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", samesite="Lax") # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", samesite="None") # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", secure=True, samesite="Strict")
- resp.set_cookie("key2", "value2", httponly=True, samesite="Strict") # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", secure=True, samesite="None")
- resp.set_cookie("key2", "value2", httponly=True, samesite="None") # $Alert[py/insecure-cookie]
- resp.set_cookie("key2", "value2", secure=True, httponly=True, samesite="Strict")
\ No newline at end of file
+ resp.set_cookie("authKey", "value1") # $Alert[py/insecure-cookie]
+ resp.set_cookie("authKey", "value2", secure=True)
+ resp.set_cookie("sessionID", "value2", httponly=True) # $Alert[py/insecure-cookie]
+ resp.set_cookie("password", "value2", samesite="Strict") # $Alert[py/insecure-cookie]
+ resp.set_cookie("notSensitive", "value3")
From 2cffb2160415eabeaa49fe1d061422d347175aca Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 23 Sep 2025 15:41:09 +0100
Subject: [PATCH 07/13] Update and fix tests
---
python/ql/lib/semmle/python/Concepts.qll | 2 +-
.../Security/CWE-1004/NonHttpOnlyCookie.ql | 4 +++-
.../Security/CWE-1275/SameSiteNoneCookie.ql | 6 ++++--
.../ql/src/Security/CWE-614/InsecureCookie.ql | 5 +++--
.../NonHttpOnlyCookie.expected | 10 +++------
.../CWE-1004-NonHttpOnlyCookie/test.py | 16 +++++---------
.../SameSiteNoneCookie.expected | 5 ++---
.../CWE-1275-SameSiteNoneCookie/test.py | 21 ++++++++-----------
8 files changed, 30 insertions(+), 39 deletions(-)
diff --git a/python/ql/lib/semmle/python/Concepts.qll b/python/ql/lib/semmle/python/Concepts.qll
index 0a6a43762476..0ca8a4dbef01 100644
--- a/python/ql/lib/semmle/python/Concepts.qll
+++ b/python/ql/lib/semmle/python/Concepts.qll
@@ -1296,7 +1296,7 @@ module Http {
exists(DataFlow::Node name |
name = [this.getNameArg(), this.getHeaderArg()] and
(
- name instanceof SensitiveDataSource
+ DataFlow::localFlow(any(SensitiveDataSource src), name)
or
name = sensitiveLookupStringConst(_)
)
diff --git a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
index fbd0ce3832c3..4c2c9c585e89 100644
--- a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
+++ b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
@@ -15,5 +15,7 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.Concepts
from Http::Server::CookieWrite cookie
-where cookie.hasHttpOnlyFlag(false)
+where
+ cookie.hasHttpOnlyFlag(false) and
+ cookie.isSensitive()
select cookie, "Cookie is added without the HttpOnly attribute properly set."
diff --git a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
index 58f0e358390f..b33f815e7155 100644
--- a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
+++ b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
@@ -3,7 +3,7 @@
* @description Cookies with `SameSite` set to `None` can allow for Cross-Site Request Forgery (CSRF) attacks.
* @kind problem
* @problem.severity warning
- * @security-severity 3.5
+ * @security-severity 4.0
* @precision high
* @id py/samesite-none-cookie
* @tags security
@@ -15,5 +15,7 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.Concepts
from Http::Server::CookieWrite cookie
-where cookie.hasSameSiteAttribute(any(Http::Server::CookieWrite::SameSiteNone v))
+where
+ cookie.hasSameSiteAttribute(any(Http::Server::CookieWrite::SameSiteNone v)) and
+ cookie.isSensitive()
select cookie, "Cookie is added with the SameSite attribute set to None."
diff --git a/python/ql/src/Security/CWE-614/InsecureCookie.ql b/python/ql/src/Security/CWE-614/InsecureCookie.ql
index 98e90c687caa..0a13e2dc378f 100644
--- a/python/ql/src/Security/CWE-614/InsecureCookie.ql
+++ b/python/ql/src/Security/CWE-614/InsecureCookie.ql
@@ -16,6 +16,7 @@ import semmle.python.dataflow.new.DataFlow
import semmle.python.Concepts
from Http::Server::CookieWrite cookie
-where cookie.hasSecureFlag(false) //and
-//cookie.isSensitive()
+where
+ cookie.hasSecureFlag(false) and
+ cookie.isSensitive()
select cookie, "Cookie is added without the Secure attribute properly set."
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
index fc1ceee53d56..1ba2ce8846d4 100644
--- a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
@@ -1,7 +1,3 @@
-| test.py:8:5:8:37 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:9:5:9:50 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:11:5:11:56 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:12:5:12:53 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:13:5:13:54 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:14:5:14:69 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:16:5:16:67 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:8:5:8:38 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:9:5:9:51 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:11:5:11:57 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
index b036cc0e45ae..6091e31ae329 100644
--- a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
@@ -5,14 +5,8 @@
@app.route("/test")
def test():
resp = make_response()
- resp.set_cookie("key1", "value1") # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", secure=True) # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", httponly=True)
- resp.set_cookie("key2", "value2", samesite="Strict") # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", samesite="Lax") # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", samesite="None") # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", secure=True, samesite="Strict") # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", httponly=True, samesite="Strict")
- resp.set_cookie("key2", "value2", secure=True, samesite="None") # $Alert[py/client-exposed-cookie]
- resp.set_cookie("key2", "value2", httponly=True, samesite="None")
- resp.set_cookie("key2", "value2", secure=True, httponly=True, samesite="Strict")
\ No newline at end of file
+ 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")
\ No newline at end of file
diff --git a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
index 71a578b328d5..63efaa86dfc3 100644
--- a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
@@ -1,3 +1,2 @@
-| test.py:13:5:13:54 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
-| test.py:16:5:16:67 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
-| test.py:17:5:17:69 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
+| test.py:10:5:10:60 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
+| test.py:13:5:13:78 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
diff --git a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
index 460594c1ab28..f1fd1fd44864 100644
--- a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
@@ -3,16 +3,13 @@
app = Flask(__name__)
@app.route("/test")
-def test():
+def test(oauth_cookie_name):
resp = make_response()
- resp.set_cookie("key1", "value1")
- resp.set_cookie("key2", "value2", secure=True)
- resp.set_cookie("key2", "value2", httponly=True)
- resp.set_cookie("key2", "value2", samesite="Strict")
- resp.set_cookie("key2", "value2", samesite="Lax")
- resp.set_cookie("key2", "value2", samesite="None") # $Alert[py/samesite-none-cookie]
- resp.set_cookie("key2", "value2", secure=True, samesite="Strict")
- resp.set_cookie("key2", "value2", httponly=True, samesite="Strict")
- resp.set_cookie("key2", "value2", secure=True, samesite="None") # $Alert[py/samesite-none-cookie]
- resp.set_cookie("key2", "value2", httponly=True, samesite="None") # $Alert[py/samesite-none-cookie]
- resp.set_cookie("key2", "value2", secure=True, httponly=True, samesite="Strict")
\ No newline at end of file
+ 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")
+
\ No newline at end of file
From 1208195d8a3db74fd0d63d9547f32d9741b53e88 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 23 Sep 2025 15:46:53 +0100
Subject: [PATCH 08/13] Align alert messages across languages.
---
python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql | 4 ++--
python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql | 4 ++--
python/ql/src/Security/CWE-614/InsecureCookie.ql | 2 +-
3 files changed, 5 insertions(+), 5 deletions(-)
diff --git a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
index 4c2c9c585e89..43f02cbcb573 100644
--- a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
+++ b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
@@ -1,5 +1,5 @@
/**
- * @name Cookie missing `HttpOnly` attribute.
+ * @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
@@ -18,4 +18,4 @@ from Http::Server::CookieWrite cookie
where
cookie.hasHttpOnlyFlag(false) and
cookie.isSensitive()
-select cookie, "Cookie is added without the HttpOnly attribute properly set."
+select cookie, "Sensitive cookie is set without HttpOnly flag."
diff --git a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
index b33f815e7155..ebad2ddbba65 100644
--- a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
+++ b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.ql
@@ -1,5 +1,5 @@
/**
- * @name Cookie with `SameSite` attribute set to `None`.
+ * @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
@@ -18,4 +18,4 @@ from Http::Server::CookieWrite cookie
where
cookie.hasSameSiteAttribute(any(Http::Server::CookieWrite::SameSiteNone v)) and
cookie.isSensitive()
-select cookie, "Cookie is added with the SameSite attribute set to None."
+select cookie, "Sensitive cookie with SameSite set to 'None'."
diff --git a/python/ql/src/Security/CWE-614/InsecureCookie.ql b/python/ql/src/Security/CWE-614/InsecureCookie.ql
index 0a13e2dc378f..603c573e17f4 100644
--- a/python/ql/src/Security/CWE-614/InsecureCookie.ql
+++ b/python/ql/src/Security/CWE-614/InsecureCookie.ql
@@ -19,4 +19,4 @@ from Http::Server::CookieWrite cookie
where
cookie.hasSecureFlag(false) and
cookie.isSensitive()
-select cookie, "Cookie is added without the Secure attribute properly set."
+select cookie, "Cookie is added to response without the 'secure' flag being set."
From 55fd7c85c6ad5b529e45db8b09fde58149ca8fb8 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 23 Sep 2025 15:50:27 +0100
Subject: [PATCH 09/13] Update documentation
---
python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp | 2 +-
python/ql/src/Security/CWE-1004/examples/InsecureCookie.py | 6 +++---
python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp | 2 +-
python/ql/src/Security/CWE-1275/examples/InsecureCookie.py | 6 +++---
python/ql/src/Security/CWE-614/examples/InsecureCookie.py | 6 +++---
5 files changed, 11 insertions(+), 11 deletions(-)
diff --git a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp
index 01c472021ad0..7addd758fca9 100644
--- a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp
+++ b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.qhelp
@@ -6,7 +6,7 @@
Cookies without the HttpOnly 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 cookie does not need to be accessed directly by client-side JS, the HttpOnly flag should be set.
+If a sensitive cookie does not need to be accessed directly by client-side JS, the HttpOnly flag should be set.
diff --git a/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py b/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
index 8ca12936a120..19d84d14537c 100644
--- a/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
+++ b/python/ql/src/Security/CWE-1004/examples/InsecureCookie.py
@@ -4,18 +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
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
index e38ef00433a4..e0cc6eade1d3 100644
--- a/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
+++ b/python/ql/src/Security/CWE-1275/SameSiteNoneCookie.qhelp
@@ -5,7 +5,7 @@
Cookies with the SameSite attribute set to 'None' 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.
+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.
diff --git a/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py b/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
index 8ca12936a120..19d84d14537c 100644
--- a/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
+++ b/python/ql/src/Security/CWE-1275/examples/InsecureCookie.py
@@ -4,18 +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
\ No newline at end of file
diff --git a/python/ql/src/Security/CWE-614/examples/InsecureCookie.py b/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
index 8ca12936a120..19d84d14537c 100644
--- a/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
+++ b/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
@@ -4,18 +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
\ No newline at end of file
From 85f886932d38a29c77122de29487f342bddd3a97 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Tue, 23 Sep 2025 15:51:31 +0100
Subject: [PATCH 10/13] Update changenote
---
python/ql/src/change-notes/2025-09-19-insecure-cookie.md | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/python/ql/src/change-notes/2025-09-19-insecure-cookie.md b/python/ql/src/change-notes/2025-09-19-insecure-cookie.md
index 58415584a96b..51c6dc6ce30f 100644
--- a/python/ql/src/change-notes/2025-09-19-insecure-cookie.md
+++ b/python/ql/src/change-notes/2025-09-19-insecure-cookie.md
@@ -1,4 +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`.
\ No newline at end of file
+* 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.
\ No newline at end of file
From 654ed9ca12dd14b186fd01420a6af5f0e027e292 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 24 Sep 2025 10:58:53 +0100
Subject: [PATCH 11/13] Update integration tests
---
.../query-suite/python-code-scanning.qls.expected | 2 ++
.../query-suite/python-security-and-quality.qls.expected | 2 ++
.../query-suite/python-security-extended.qls.expected | 2 ++
3 files changed, 6 insertions(+)
diff --git a/python/ql/integration-tests/query-suite/python-code-scanning.qls.expected b/python/ql/integration-tests/query-suite/python-code-scanning.qls.expected
index 4db5af9c1a2f..90885318d0d8 100644
--- a/python/ql/integration-tests/query-suite/python-code-scanning.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-code-scanning.qls.expected
@@ -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
diff --git a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected
index faa4204c3c71..11b75dd0ee39 100644
--- a/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-security-and-quality.qls.expected
@@ -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
diff --git a/python/ql/integration-tests/query-suite/python-security-extended.qls.expected b/python/ql/integration-tests/query-suite/python-security-extended.qls.expected
index 1b255c6a0d05..d677e65c3cfe 100644
--- a/python/ql/integration-tests/query-suite/python-security-extended.qls.expected
+++ b/python/ql/integration-tests/query-suite/python-security-extended.qls.expected
@@ -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
From 9f5bfeb7f411a880943ba11665f4984a23014ab7 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Wed, 24 Sep 2025 15:03:40 +0100
Subject: [PATCH 12/13] Update test output
---
.../CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected | 6 +++---
.../CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected | 4 ++--
.../Security/CWE-614-InsecureCookie/InsecureCookie.expected | 6 +++---
3 files changed, 8 insertions(+), 8 deletions(-)
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
index 1ba2ce8846d4..20c5912c6e19 100644
--- a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
@@ -1,3 +1,3 @@
-| test.py:8:5:8:38 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:9:5:9:51 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
-| test.py:11:5:11:57 | ControlFlowNode for Attribute() | Cookie is added without the HttpOnly attribute properly set. |
+| test.py:8:5:8:38 | ControlFlowNode for Attribute() | Sensitive cookie is set without HttpOnly flag. |
+| test.py:9:5:9:51 | ControlFlowNode for Attribute() | Sensitive cookie is set without HttpOnly flag. |
+| test.py:11:5:11:57 | ControlFlowNode for Attribute() | Sensitive cookie is set without HttpOnly flag. |
diff --git a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
index 63efaa86dfc3..7a8e83a732c6 100644
--- a/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
@@ -1,2 +1,2 @@
-| test.py:10:5:10:60 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
-| test.py:13:5:13:78 | ControlFlowNode for Attribute() | Cookie is added with the SameSite attribute set to None. |
+| 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'. |
diff --git a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
index 8d33578e5330..3b07bc6d9ebb 100644
--- a/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-614-InsecureCookie/InsecureCookie.expected
@@ -1,3 +1,3 @@
-| test.py:8:5:8:40 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:10:5:10:57 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute properly set. |
-| test.py:11:5:11:60 | ControlFlowNode for Attribute() | Cookie is added without the Secure attribute 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. |
From cb7b1efe8182031430371f8844aecf69e927de90 Mon Sep 17 00:00:00 2001
From: Joe Farebrother
Date: Thu, 25 Sep 2025 09:52:27 +0100
Subject: [PATCH 13/13] Update alert message
---
python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql | 2 +-
.../CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected | 6 +++---
2 files changed, 4 insertions(+), 4 deletions(-)
diff --git a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
index 43f02cbcb573..01056daaf788 100644
--- a/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
+++ b/python/ql/src/Security/CWE-1004/NonHttpOnlyCookie.ql
@@ -18,4 +18,4 @@ from Http::Server::CookieWrite cookie
where
cookie.hasHttpOnlyFlag(false) and
cookie.isSensitive()
-select cookie, "Sensitive cookie is set without HttpOnly flag."
+select cookie, "Sensitive server cookie is set without HttpOnly flag."
diff --git a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
index 20c5912c6e19..7af8af8d8708 100644
--- a/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
@@ -1,3 +1,3 @@
-| test.py:8:5:8:38 | ControlFlowNode for Attribute() | Sensitive cookie is set without HttpOnly flag. |
-| test.py:9:5:9:51 | ControlFlowNode for Attribute() | Sensitive cookie is set without HttpOnly flag. |
-| test.py:11:5:11:57 | ControlFlowNode for Attribute() | Sensitive cookie is set without HttpOnly flag. |
+| 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. |