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. |