Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
513ae2a
Rust: Add tests for insecure cookies.
geoffw0 Sep 9, 2025
7e75c1d
Rust: Add very basic query prototype.
geoffw0 Sep 11, 2025
d52b668
Rust: Add security-severity tag.
geoffw0 Sep 11, 2025
eadf922
Rust: Use models-as-data, add source/sink/flow models.
geoffw0 Sep 15, 2025
257a1b0
Rust: Refactor sources, sinks into an extensions source file.
geoffw0 Sep 17, 2025
2654aff
Rust: Account for the 'secure' and 'partitioned' attributes.
geoffw0 Sep 19, 2025
a3ed83b
Rust: Make state transition / barrier nodes more reliable.
geoffw0 Sep 19, 2025
94afc82
Rust: Fix an issue with the local flow.
geoffw0 Sep 22, 2025
bd07350
Rust: Add qhelp and examples.
geoffw0 Sep 19, 2025
4662e42
Rust: Add examples as tests (and fix them).
geoffw0 Sep 22, 2025
ae90253
Rust: Add the new query to suite lists.
geoffw0 Sep 19, 2025
3de1911
Rust: Change note.
geoffw0 Sep 19, 2025
cc9c414
Apply suggestions from code review
geoffw0 Sep 22, 2025
5b4632b
Apply suggestions from code review
geoffw0 Sep 22, 2025
43ac75e
Rust: Address another tiny suggestion from review.
geoffw0 Sep 22, 2025
6362884
Rust: Autoformat.
geoffw0 Sep 22, 2025
86c8c3c
Rust: Fix warning by making the query a path-problem.
geoffw0 Sep 22, 2025
266624d
Rust: The test needs to have Source tags now.
geoffw0 Sep 22, 2025
21fe142
Update rust/ql/src/queries/security/CWE-614/InsecureCookie.qhelp
geoffw0 Sep 26, 2025
57f8487
Rust: Split off cookieOptionalBarrier predicate (as suggested) and ex…
geoffw0 Sep 26, 2025
f458149
Rust: Remove a sentance from the qhelp.
geoffw0 Sep 26, 2025
77e7898
Rust: Use US spelling in comment.
geoffw0 Sep 26, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
ql/rust/ql/src/queries/security/CWE-825/AccessInvalidPointer.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
ql/rust/ql/src/queries/security/CWE-696/BadCtorInitialization.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextLogging.ql
ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-328/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
ql/rust/ql/src/queries/security/CWE-825/AccessAfterLifetime.ql
Expand Down
25 changes: 25 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/biscotti.model.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,32 @@
# Models for the `biscotti` crate.
extensions:
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
data:
- ["<biscotti::response_cookie::ResponseCookie>::new", "ReturnValue", "cookie-create", "manual"]
- ["<biscotti::response_cookie::ResponseCookie as core::convert::From>::from", "ReturnValue", "cookie-create", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["<biscotti::response_cookies::ResponseCookies>::insert", "Argument[0]", "cookie-use", "manual"]
- ["<biscotti::crypto::master::Key>::from", "Argument[0]", "credentials-key", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: summaryModel
data:
- ["<biscotti::response_cookie::ResponseCookie>::set_secure", "Argument[self].OptionalBarrier[cookie-secure-arg0]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_partitioned", "Argument[self].OptionalBarrier[cookie-partitioned-arg0]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_name", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_value", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_http_only", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_same_site", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_max_age", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_path", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::unset_path", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_domain", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::unset_domain", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::set_expires", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::unset_expires", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<biscotti::response_cookie::ResponseCookie>::make_permanent", "Argument[self]", "ReturnValue", "taint", "manual"]
33 changes: 33 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/cookie.model.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,40 @@
# Models for the `cookie` crate.
extensions:
- addsTo:
pack: codeql/rust-all
extensible: sourceModel
data:
- ["<cookie::Cookie>::build", "ReturnValue", "cookie-create", "manual"]
- ["<cookie::builder::CookieBuilder>::new", "ReturnValue", "cookie-create", "manual"]
- ["<cookie::Cookie>::new", "ReturnValue", "cookie-create", "manual"]
- ["<cookie::Cookie>::named", "ReturnValue", "cookie-create", "manual"]
- ["<cookie::Cookie as core::convert::From>::from", "ReturnValue", "cookie-create", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["<cookie::builder::CookieBuilder>::build", "Argument[self]", "cookie-use", "manual"]
- ["<cookie::builder::CookieBuilder>::finish", "Argument[self]", "cookie-use", "manual"]
- ["<cookie::jar::CookieJar>::add", "Argument[0]", "cookie-use", "manual"]
- ["<cookie::jar::CookieJar>::add_original", "Argument[0]", "cookie-use", "manual"]
- ["<cookie::secure::signed::SignedJar>::add", "Argument[0]", "cookie-use", "manual"]
- ["<cookie::secure::signed::SignedJar>::add_original", "Argument[0]", "cookie-use", "manual"]
- ["<cookie::secure::private::PrivateJar>::add", "Argument[0]", "cookie-use", "manual"]
- ["<cookie::secure::private::PrivateJar>::add_original", "Argument[0]", "cookie-use", "manual"]
- ["<cookie::secure::key::Key>::from", "Argument[0].Reference", "credentials-key", "manual"]
- addsTo:
pack: codeql/rust-all
extensible: summaryModel
data:
- ["<cookie::builder::CookieBuilder>::secure", "Argument[self].OptionalBarrier[cookie-secure-arg0]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::partitioned", "Argument[self].OptionalBarrier[cookie-partitioned-arg0]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::expires", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::max_age", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::domain", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::path", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::http_only", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::same_site", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::permanent", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::builder::CookieBuilder>::removal", "Argument[self]", "ReturnValue", "taint", "manual"]
- ["<cookie::Cookie>::set_secure", "Argument[self].OptionalBarrier[cookie-secure-arg0]", "ReturnValue", "taint", "manual"]
- ["<cookie::Cookie>::set_partitioned", "Argument[self].OptionalBarrier[cookie-partitioned-arg0]", "ReturnValue", "taint", "manual"]
114 changes: 114 additions & 0 deletions rust/ql/lib/codeql/rust/security/InsecureCookieExtensions.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
/**
* Provides classes and predicates for reasoning about insecure cookie
* vulnerabilities.
*/

import rust
private import codeql.rust.dataflow.DataFlow
private import codeql.rust.dataflow.FlowSource
private import codeql.rust.dataflow.FlowSink
private import codeql.rust.Concepts
private import codeql.rust.dataflow.internal.DataFlowImpl as DataFlowImpl
private import codeql.rust.dataflow.internal.Node
private import codeql.rust.controlflow.BasicBlocks

/**
* Provides default sources, sinks and barriers for detecting insecure
* cookie vulnerabilities, as well as extension points for adding your own.
*/
module InsecureCookie {
/**
* A data flow source for insecure cookie vulnerabilities.
*/
abstract class Source extends DataFlow::Node { }

/**
* A data flow sink for insecure cookie vulnerabilities.
*/
abstract class Sink extends QuerySink::Range {
override string getSinkType() { result = "InsecureCookie" }
}

/**
* A barrier for insecure cookie vulnerabilities.
*/
abstract class Barrier extends DataFlow::Node { }

/**
* A source for insecure cookie vulnerabilities from model data.
*/
private class ModelsAsDataSource extends Source {
ModelsAsDataSource() { sourceNode(this, "cookie-create") }
}

/**
* A sink for insecure cookie vulnerabilities from model data.
*/
private class ModelsAsDataSink extends Sink {
ModelsAsDataSink() { sinkNode(this, "cookie-use") }
}

/**
* Holds if a models-as-data optional barrier for cookies is specified for `summaryNode`,
* with arguments `attrib` (`secure` or `partitioned`) and `arg` (argument index). For example,
* if a summary has input:
* ```
* [..., Argument[self].OptionalBarrier[cookie-secure-arg0], ...]
* ```
* then `attrib` is `secure` and `arg` is `0`.
*
* The meaning here is that a call to the function summarized by `summaryNode` would set
* the cookie attribute `attrib` to the value of argument `arg`. This may in practice be
* interpretted as a barrier to flow (when the cookie is made secure) or as a source (when
* the cookie is made insecure).
*/
private predicate cookieOptionalBarrier(FlowSummaryNode summaryNode, string attrib, int arg) {
exists(string barrierName |
DataFlowImpl::optionalBarrier(summaryNode, barrierName) and
attrib = barrierName.regexpCapture("cookie-(secure|partitioned)-arg([0-9]+)", 1) and
arg = barrierName.regexpCapture("cookie-(secure|partitioned)-arg([0-9]+)", 2).toInt()
)
}

/**
* Holds if cookie attribute `attrib` (`secure` or `partitioned`) is set to `value`
* (`true` or `false`) at `node`. For example, the call:
* ```
* cookie.secure(true)
* ```
* sets the `"secure"` attribute to `true`. A value that cannot be determined is treated
* as `false`.
*/
predicate cookieSetNode(DataFlow::Node node, string attrib, boolean value) {
exists(FlowSummaryNode summaryNode, CallExprBase ce, int arg, DataFlow::Node argNode |
// decode the models-as-data `OptionalBarrier`
cookieOptionalBarrier(summaryNode, attrib, arg) and
// find a call and arg referenced by this optional barrier
ce.getStaticTarget() = summaryNode.getSummarizedCallable() and
ce.getArg(arg) = argNode.asExpr().getExpr() and
// check if the argument is always `true`
(
if
forex(DataFlow::Node argSourceNode, BooleanLiteralExpr argSourceValue |
DataFlow::localFlow(argSourceNode, argNode) and
argSourceValue = argSourceNode.asExpr().getExpr()
|
argSourceValue.getTextValue() = "true"
)
then value = true // `true` flows to here
else value = false // `false`, unknown, or multiple values
) and
// and find the node where this happens (we can't just use the flow summary node, since its
// shared across all calls to the modeled function, we need a node specific to this call)
(
node.asExpr().getExpr() = ce.(MethodCallExpr).getReceiver() // e.g. `a` in `a.set_secure(true)`
or
exists(BasicBlock bb, int i |
// associated SSA node
node.(SsaNode).asDefinition().definesAt(_, bb, i) and
ce.(MethodCallExpr).getReceiver() = bb.getNode(i).getAstNode()
)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These nodes (which function as barriers) are essentially duplicated at the corresponding SSA dataflow nodes, this shouldn't be necessary but it currently is necessary for the state-setting calls (e.g. test line 61) to work properly.

)
)
}
}
4 changes: 4 additions & 0 deletions rust/ql/src/change-notes/2025-09-19-insecure-cookie.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
---
category: newQuery
---
* Added a new query, `rust/insecure-cookie`, to detect cookies created without the 'Secure' attribute.
33 changes: 33 additions & 0 deletions rust/ql/src/queries/security/CWE-614/InsecureCookie.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>
<overview>

<p>Failing to set the 'Secure' attribute on a cookie allows it to be transmitted over an unencrypted (HTTP) connection. If an attacker can observe a user's network traffic, they can access sensitive information in the cookie and potentially use it to impersonate the user.</p>

</overview>
<recommendation>

<p>Always set the cookie 'Secure' attribute so that the browser only sends the cookie over HTTPS.</p>

</recommendation>
<example>

<p>The following example creates a cookie using the <a href="https://crates.io/crates/cookie">cookie</a> crate without the 'Secure' attribute:</p>

<sample src="InsecureCookieBad.rs" />

<p>In the fixed example, we either call <code>secure(true)</code> on the <code>CookieBuilder</code> or <code>set_secure(true)</code> on the <code>Cookie</code> itself:</p>

<sample src="InsecureCookieGood.rs" />

</example>
<references>

<li>MDN Web Docs: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies">Using HTTP cookies</a>.</li>
<li>OWASP Cheat Sheet Series: <a href="https://cheatsheetseries.owasp.org/cheatsheets/Session_Management_Cheat_Sheet.html#transport-layer-security">Session Management Cheat Sheet - Transport Layer Security</a>.</li>
<li>MDN Web Docs: <a href="https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie#secure">Set-Cookie header - Secure</a>.</li>

</references>
</qhelp>
90 changes: 90 additions & 0 deletions rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/**
* @name 'Secure' attribute is not set to true
* @description Omitting the 'Secure' attribute allows data to be transmitted insecurely
* using HTTP. Always set 'Secure' to 'true' to ensure that HTTPS
* is used at all times.
* @kind path-problem
* @problem.severity error
* @security-severity 7.5
* @precision high
* @id rust/insecure-cookie
* @tags security
* external/cwe/cwe-319
* external/cwe/cwe-614
*/

import rust
import codeql.rust.dataflow.DataFlow
import codeql.rust.dataflow.TaintTracking
import codeql.rust.security.InsecureCookieExtensions

/**
* A data flow configuration for tracking values representing cookies without the
* 'secure' attribute set. This is the primary data flow configuration for this
* query.
*/
module InsecureCookieConfig implements DataFlow::ConfigSig {
import InsecureCookie

predicate isSource(DataFlow::Node node) {
// creation of a cookie or cookie configuration with default, insecure settings
node instanceof Source
or
// setting the 'secure' attribute to false (or an unknown value)
cookieSetNode(node, "secure", false)
}

predicate isSink(DataFlow::Node node) {
// use of a cookie or cookie configuration
node instanceof Sink
}

predicate isBarrier(DataFlow::Node node) {
// setting the 'secure' attribute to true
cookieSetNode(node, "secure", true)
or
node instanceof Barrier
}

predicate observeDiffInformedIncrementalMode() { any() }
}

/**
* A data flow configuration for tracking values representing cookies with the
* 'partitioned' attribute set. This is a secondary data flow configuration used
* to filter out unwanted results.
*/
module PartitionedCookieConfig implements DataFlow::ConfigSig {
import InsecureCookie

predicate isSource(DataFlow::Node node) {
// setting the 'partitioned' attribute to true
cookieSetNode(node, "partitioned", true)
}

predicate isSink(DataFlow::Node node) {
// use of a cookie or cookie configuration
node instanceof Sink
}

predicate isBarrier(DataFlow::Node node) {
// setting the 'partitioned' attribute to false (or an unknown value)
cookieSetNode(node, "partitioned", false)
or
node instanceof Barrier
}

predicate observeDiffInformedIncrementalMode() { any() }
}

module InsecureCookieFlow = TaintTracking::Global<InsecureCookieConfig>;

module PartitionedCookieFlow = TaintTracking::Global<PartitionedCookieConfig>;

import InsecureCookieFlow::PathGraph

from InsecureCookieFlow::PathNode sourceNode, InsecureCookieFlow::PathNode sinkNode
where
InsecureCookieFlow::flowPath(sourceNode, sinkNode) and
not PartitionedCookieFlow::flow(_, sinkNode.getNode())
select sinkNode.getNode(), sourceNode, sinkNode, "Cookie attribute 'Secure' is not set to true."
6 changes: 6 additions & 0 deletions rust/ql/src/queries/security/CWE-614/InsecureCookieBad.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
use cookie::Cookie;

// BAD: creating a cookie without specifying the `secure` attribute
let cookie = Cookie::build(("session", "abcd1234")).build();
let mut jar = cookie::CookieJar::new();
jar.add(cookie.clone());
11 changes: 11 additions & 0 deletions rust/ql/src/queries/security/CWE-614/InsecureCookieGood.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
use cookie::Cookie;

// GOOD: set the `CookieBuilder` 'Secure' attribute so that the cookie is only sent over HTTPS
let secure_cookie = Cookie::build(("session", "abcd1234")).secure(true).build();
let mut jar = cookie::CookieJar::new();
jar.add(secure_cookie.clone());

// GOOD: alternatively, set the 'Secure' attribute on an existing `Cookie`
let mut secure_cookie2 = Cookie::new("session", "abcd1234");
secure_cookie2.set_secure(true);
jar.add(secure_cookie2);
5 changes: 3 additions & 2 deletions rust/ql/src/queries/summary/Stats.qll
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ private import codeql.rust.security.AccessInvalidPointerExtensions
private import codeql.rust.security.CleartextLoggingExtensions
private import codeql.rust.security.CleartextStorageDatabaseExtensions
private import codeql.rust.security.CleartextTransmissionExtensions
private import codeql.rust.security.RequestForgeryExtensions
private import codeql.rust.security.HardcodedCryptographicValueExtensions
private import codeql.rust.security.InsecureCookieExtensions
private import codeql.rust.security.LogInjectionExtensions
private import codeql.rust.security.RequestForgeryExtensions
private import codeql.rust.security.SqlInjectionExtensions
private import codeql.rust.security.TaintedPathExtensions
private import codeql.rust.security.UncontrolledAllocationSizeExtensions
private import codeql.rust.security.WeakSensitiveDataHashingExtensions
private import codeql.rust.security.HardcodedCryptographicValueExtensions

/**
* Gets a count of the total number of lines of code in the database.
Expand Down
Loading