-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/InsecureCookie.ql b/python/ql/src/Security/CWE-614/InsecureCookie.ql
index 260bd303310d..603c573e17f4 100644
--- a/python/ql/src/Security/CWE-614/InsecureCookie.ql
+++ b/python/ql/src/Security/CWE-614/InsecureCookie.ql
@@ -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."
diff --git a/python/ql/src/Security/CWE-614/examples/InsecureCookie.py b/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
index 07cca6c3fcee..19d84d14537c 100644
--- a/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
+++ b/python/ql/src/Security/CWE-614/examples/InsecureCookie.py
@@ -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
\ No newline at end of file
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..51c6dc6ce30f
--- /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`. These queries also now only alert for cases in which the cookie is detected to contain sensitive data.
\ No newline at end of file
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..7af8af8d8708
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/NonHttpOnlyCookie.expected
@@ -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. |
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..6091e31ae329
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1004-NonHttpOnlyCookie/test.py
@@ -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")
\ 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..7a8e83a732c6
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/SameSiteNoneCookie.expected
@@ -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'. |
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..f1fd1fd44864
--- /dev/null
+++ b/python/ql/test/query-tests/Security/CWE-1275-SameSiteNoneCookie/test.py
@@ -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")
+
\ 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..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,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. |
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..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
@@ -1,20 +1,12 @@
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("key2", "value2", secure=True, samesite="Strict")
- resp.set_cookie("key2", "value2", httponly=True, samesite="Strict")
- resp.set_cookie("key2", "value2", secure=True, samesite="None")
- 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("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")