From 3a3bea52802e1280d237848743fba5702fc9b458 Mon Sep 17 00:00:00 2001 From: Lalith Chaitanya Mangalagiri Date: Tue, 11 Aug 2026 21:33:14 -0700 Subject: [PATCH 001/140] Rust: Add unsafe deserialization query (CWE-502) Detects user-controlled data flowing into deserialization functions (serde_json, bincode, rmp_serde, ciborium, serde_yaml, toml). - Extension library with sources, sinks, and barriers - Models-as-data sink definitions - Query help (.qhelp) with examples - Test cases with inline expectations Query ID: rust/unsafe-deserialization --- .../codeql/rust/frameworks/serde.model.yml | 24 +++++++ .../UnsafeDeserializationExtensions.qll | 64 ++++++++++++++++++ .../CWE-502/UnsafeDeserialization.qhelp | 46 +++++++++++++ .../security/CWE-502/UnsafeDeserialization.ql | 42 ++++++++++++ .../CWE-502/UnsafeDeserializationBad.rs | 12 ++++ .../CWE-502/UnsafeDeserializationGood.rs | 30 +++++++++ .../CWE-502/UnsafeDeserialization.qlref | 4 ++ .../test/query-tests/security/CWE-502/main.rs | 66 +++++++++++++++++++ .../query-tests/security/CWE-502/options.yml | 6 ++ 9 files changed, 294 insertions(+) create mode 100644 rust/ql/lib/codeql/rust/frameworks/serde.model.yml create mode 100644 rust/ql/lib/codeql/rust/security/UnsafeDeserializationExtensions.qll create mode 100644 rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp create mode 100644 rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.ql create mode 100644 rust/ql/src/queries/security/CWE-502/UnsafeDeserializationBad.rs create mode 100644 rust/ql/src/queries/security/CWE-502/UnsafeDeserializationGood.rs create mode 100644 rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref create mode 100644 rust/ql/test/query-tests/security/CWE-502/main.rs create mode 100644 rust/ql/test/query-tests/security/CWE-502/options.yml diff --git a/rust/ql/lib/codeql/rust/frameworks/serde.model.yml b/rust/ql/lib/codeql/rust/frameworks/serde.model.yml new file mode 100644 index 000000000000..3d3c8d63ef85 --- /dev/null +++ b/rust/ql/lib/codeql/rust/frameworks/serde.model.yml @@ -0,0 +1,24 @@ +extensions: + - addsTo: + pack: codeql/rust-all + extensible: sinkModel + data: + # serde_json deserialization functions + - ["crate::serde_json::from_str", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::serde_json::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::serde_json::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::serde_json::from_value", "Argument[0]", "unsafe-deserialization", "manual"] + # bincode deserialization functions + - ["crate::bincode::deserialize", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::bincode::deserialize_from", "Argument[0]", "unsafe-deserialization", "manual"] + # rmp_serde (MessagePack) deserialization functions + - ["crate::rmp_serde::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::rmp_serde::from_read", "Argument[0]", "unsafe-deserialization", "manual"] + # ciborium (CBOR) deserialization functions + - ["crate::ciborium::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] + # serde_yaml deserialization functions + - ["crate::serde_yaml::from_str", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::serde_yaml::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] + - ["crate::serde_yaml::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] + # toml deserialization + - ["crate::toml::from_str", "Argument[0]", "unsafe-deserialization", "manual"] diff --git a/rust/ql/lib/codeql/rust/security/UnsafeDeserializationExtensions.qll b/rust/ql/lib/codeql/rust/security/UnsafeDeserializationExtensions.qll new file mode 100644 index 000000000000..70b0c55184c0 --- /dev/null +++ b/rust/ql/lib/codeql/rust/security/UnsafeDeserializationExtensions.qll @@ -0,0 +1,64 @@ +/** + * Provides classes and predicates for reasoning about unsafe deserialization + * vulnerabilities (CWE-502). + */ + +import rust +private import codeql.rust.dataflow.DataFlow +private import codeql.rust.dataflow.FlowSink +private import codeql.rust.dataflow.FlowBarrier +private import codeql.rust.Concepts +private import codeql.rust.security.Barriers as Barriers + +/** + * Provides default sources, sinks and barriers for detecting unsafe deserialization + * vulnerabilities, as well as extension points for adding your own. + */ +module UnsafeDeserialization { + /** + * A data flow source for unsafe deserialization vulnerabilities. + */ + abstract class Source extends DataFlow::Node { } + + /** + * A data flow sink for unsafe deserialization vulnerabilities. + */ + abstract class Sink extends QuerySink::Range { + override string getSinkType() { result = "UnsafeDeserialization" } + } + + /** + * A barrier for unsafe deserialization vulnerabilities. + */ + abstract class Barrier extends DataFlow::Node { } + + /** + * An active threat-model source, considered as a flow source. + */ + private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { } + + /** + * A sink for unsafe deserialization from model data. + */ + private class ModelsAsDataSink extends Sink { + ModelsAsDataSink() { sinkNode(this, "unsafe-deserialization") } + } + + /** + * A barrier for unsafe deserialization from model data. + */ + private class ModelsAsDataBarrier extends Barrier { + ModelsAsDataBarrier() { barrierNode(this, "unsafe-deserialization") } + } + + /** + * A barrier for unsafe deserialization for nodes whose type is a numeric + * type, which is unlikely to expose any vulnerability. + */ + private class NumericTypeBarrier extends Barrier instanceof Barriers::NumericTypeBarrier { } + + private class BooleanTypeBarrier extends Barrier instanceof Barriers::BooleanTypeBarrier { } + + private class FieldlessEnumTypeBarrier extends Barrier instanceof Barriers::FieldlessEnumTypeBarrier + { } +} diff --git a/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp b/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp new file mode 100644 index 000000000000..daa155b51fa9 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp @@ -0,0 +1,46 @@ + + + + +

+Deserializing untrusted data without validation can allow an attacker to cause denial of service, consume excessive resources, or in some cases execute arbitrary code. In Rust, while memory safety mitigates some risks, deserializing untrusted data with libraries like serde, bincode, or rmp-serde can still lead to panics, excessive memory allocation, or logic bugs when trait objects or polymorphic types are involved. +

+ +
+ + +

+Avoid deserializing untrusted data with formats that allow unbounded allocation or polymorphic dispatch. Prefer formats with schema validation (like Protocol Buffers) when processing untrusted input. If using serde, consider: +

+
    +
  • Validating input size before deserialization.
  • +
  • Using #[serde(deny_unknown_fields)] to reject unexpected data.
  • +
  • Avoiding #[typetag] or trait object deserialization with untrusted input.
  • +
  • Using bounded containers (e.g., limiting Vec length via custom deserializers).
  • +
+ +
+ + +

+In the following example, data from an HTTP request is directly deserialized without any validation. An attacker could send a crafted payload that causes excessive memory allocation or other unintended behavior. +

+ + + +

+A safer approach validates the input size and uses strict deserialization settings: +

+ + + +
+ + +
  • OWASP: Deserialization of untrusted data.
  • +
  • CWE-502: Deserialization of Untrusted Data.
  • + +
    +
    diff --git a/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.ql b/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.ql new file mode 100644 index 000000000000..ec6490fabee4 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.ql @@ -0,0 +1,42 @@ +/** + * @name Deserialization of user-controlled data + * @description Deserializing user-controlled data may allow an attacker to trigger unexpected + * code execution, denial of service, or other harmful effects. + * @kind path-problem + * @problem.severity error + * @security-severity 9.8 + * @precision high + * @id rust/unsafe-deserialization + * @tags security + * external/cwe/cwe-502 + */ + +import rust +import codeql.rust.dataflow.DataFlow +import codeql.rust.dataflow.TaintTracking +import codeql.rust.security.UnsafeDeserializationExtensions + +/** + * A taint configuration for detecting unsafe deserialization vulnerabilities. + */ +module UnsafeDeserializationConfig implements DataFlow::ConfigSig { + import UnsafeDeserialization + + predicate isSource(DataFlow::Node node) { node instanceof Source } + + predicate isSink(DataFlow::Node node) { node instanceof Sink } + + predicate isBarrier(DataFlow::Node barrier) { barrier instanceof Barrier } + + predicate observeDiffInformedIncrementalMode() { any() } +} + +module UnsafeDeserializationFlow = TaintTracking::Global; + +import UnsafeDeserializationFlow::PathGraph + +from UnsafeDeserializationFlow::PathNode sourceNode, UnsafeDeserializationFlow::PathNode sinkNode +where UnsafeDeserializationFlow::flowPath(sourceNode, sinkNode) +select sinkNode.getNode(), sourceNode, sinkNode, + "This deserialization operation processes $@ without validation.", sourceNode.getNode(), + "user-provided data" diff --git a/rust/ql/src/queries/security/CWE-502/UnsafeDeserializationBad.rs b/rust/ql/src/queries/security/CWE-502/UnsafeDeserializationBad.rs new file mode 100644 index 000000000000..8e4f2c6b2c0a --- /dev/null +++ b/rust/ql/src/queries/security/CWE-502/UnsafeDeserializationBad.rs @@ -0,0 +1,12 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +struct UserData { + name: String, + items: Vec, +} + +fn handle_request(body: &[u8]) -> UserData { + // BAD: deserializing user-controlled data without size validation + serde_json::from_slice(body).unwrap() +} diff --git a/rust/ql/src/queries/security/CWE-502/UnsafeDeserializationGood.rs b/rust/ql/src/queries/security/CWE-502/UnsafeDeserializationGood.rs new file mode 100644 index 000000000000..a7d69fb48a56 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-502/UnsafeDeserializationGood.rs @@ -0,0 +1,30 @@ +use serde::Deserialize; + +const MAX_BODY_SIZE: usize = 1024 * 1024; // 1 MB limit + +#[derive(Deserialize)] +#[serde(deny_unknown_fields)] +struct UserData { + name: String, + #[serde(deserialize_with = "bounded_vec")] + items: Vec, +} + +fn bounded_vec<'de, D>(deserializer: D) -> Result, D::Error> +where + D: serde::Deserializer<'de>, +{ + let v = Vec::::deserialize(deserializer)?; + if v.len() > 100 { + return Err(serde::de::Error::custom("too many items")); + } + Ok(v) +} + +fn handle_request(body: &[u8]) -> Result { + // GOOD: validate input size before deserialization, use bounded containers + if body.len() > MAX_BODY_SIZE { + return Err("payload too large".to_string()); + } + serde_json::from_slice(body).map_err(|e| e.to_string()) +} diff --git a/rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref b/rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref new file mode 100644 index 000000000000..26ecd83ac0b3 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.qlref @@ -0,0 +1,4 @@ +query: queries/security/CWE-502/UnsafeDeserialization.ql +postprocess: + - utils/test/PrettyPrintModels.ql + - utils/test/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/query-tests/security/CWE-502/main.rs b/rust/ql/test/query-tests/security/CWE-502/main.rs new file mode 100644 index 000000000000..5c607982c36b --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-502/main.rs @@ -0,0 +1,66 @@ +use serde::Deserialize; + +#[derive(Deserialize)] +struct UserData { + name: String, + items: Vec, +} + +#[derive(Deserialize)] +struct Config { + setting: String, +} + +fn test_serde_json_deserialization() { + let remote_bytes = reqwest::blocking::get("http://example.com/") // $ Source=remote1 + .unwrap() + .bytes() + .unwrap(); + let remote_string = reqwest::blocking::get("http://example.com/") // $ Source=remote2 + .unwrap() + .text() + .unwrap_or(String::from("{}")); + let const_string = String::from(r#"{"name": "test", "items": []}"#); + + // --- safe cases --- + + // Constant data deserialization + let _safe: UserData = serde_json::from_str(&const_string).unwrap(); // safe + + // --- unsafe cases --- + + // Remote bytes directly deserialized + let _unsafe1: UserData = serde_json::from_slice(&remote_bytes).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote1 + + // Remote string directly deserialized + let _unsafe2: UserData = serde_json::from_str(&remote_string).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote2 +} + +fn test_bincode_deserialization() { + let remote_bytes = reqwest::blocking::get("http://example.com/data") // $ Source=remote3 + .unwrap() + .bytes() + .unwrap(); + + // Unsafe: remote data deserialized with bincode + let _unsafe: Config = bincode::deserialize(&remote_bytes).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote3 +} + +fn test_safe_with_validation() { + let remote_string = reqwest::blocking::get("http://example.com/") // $ Source=remote4 + .unwrap() + .text() + .unwrap_or(String::from("{}")); + + // Safe: size check before deserialization (still flagged as the barrier + // is not modeled as a data flow barrier, but demonstrates the pattern) + if remote_string.len() < 1024 { + let _data: UserData = serde_json::from_str(&remote_string).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote4 + } +} + +fn main() { + test_serde_json_deserialization(); + test_bincode_deserialization(); + test_safe_with_validation(); +} diff --git a/rust/ql/test/query-tests/security/CWE-502/options.yml b/rust/ql/test/query-tests/security/CWE-502/options.yml new file mode 100644 index 000000000000..f3664c3c1ba7 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-502/options.yml @@ -0,0 +1,6 @@ +qltest_cargo_check: true +qltest_dependencies: + - reqwest = { version = "0.12.9", features = ["blocking"] } + - serde = { version = "1", features = ["derive"] } + - serde_json = { version = "1" } + - bincode = { version = "1" } From 1c65c98ec354f009ea14aeb6d04cff219c9acc0c Mon Sep 17 00:00:00 2001 From: Lalith Chaitanya Mangalagiri Date: Tue, 11 Aug 2026 21:48:38 -0700 Subject: [PATCH 002/140] Add change note for rust/unsafe-deserialization query --- rust/ql/src/change-notes/2026-08-11-unsafe-deserialization.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 rust/ql/src/change-notes/2026-08-11-unsafe-deserialization.md diff --git a/rust/ql/src/change-notes/2026-08-11-unsafe-deserialization.md b/rust/ql/src/change-notes/2026-08-11-unsafe-deserialization.md new file mode 100644 index 000000000000..6e78b9a8606a --- /dev/null +++ b/rust/ql/src/change-notes/2026-08-11-unsafe-deserialization.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* Added a new query, `rust/unsafe-deserialization`, to detect deserialization of user-controlled data. From e78e6b6a8c5861e2b1a0c73e0114fd389d7435ff Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Wed, 14 Oct 2026 11:21:00 +0100 Subject: [PATCH 003/140] Add string equality to ListOfConstantsSanitizer --- .../2026-10-14-constant-value-guards.md | 4 + .../security/ListOfConstantsSanitizer.qll | 43 ++- .../code/java/security/RequestForgery.qll | 15 +- .../test/library-tests/listofconstants/A.java | 24 ++ .../listofconstants/test.expected | 2 + .../semmle/tests/HostComparisonRedirect.java | 25 ++ .../CWE-601/semmle/tests/UrlRedirect.expected | 274 ++++++++-------- .../security/CWE-918/HostComparison.java | 25 ++ .../security/CWE-918/RequestForgery.expected | 307 +++++++++--------- 9 files changed, 416 insertions(+), 303 deletions(-) create mode 100644 java/ql/lib/change-notes/2026-10-14-constant-value-guards.md diff --git a/java/ql/lib/change-notes/2026-10-14-constant-value-guards.md b/java/ql/lib/change-notes/2026-10-14-constant-value-guards.md new file mode 100644 index 000000000000..ee8e09960064 --- /dev/null +++ b/java/ql/lib/change-notes/2026-10-14-constant-value-guards.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Java taint-tracking queries now recognize equality and case-insensitive equality checks against fixed string constants as sanitizers. The queries `java/ssrf` and `java/unvalidated-url-redirection` also recognize checks of `URI.getHost()` against fixed collections of strings, including when the host is normalized to upper or lower case. diff --git a/java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll b/java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll index b00c5c99405a..480191c1e412 100644 --- a/java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll +++ b/java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll @@ -1,7 +1,4 @@ -/** - * Provides a default taint sanitizer identifying comparisons against lists of - * compile-time constants. - */ +/** Provides a default taint sanitizer identifying checks against compile-time constants. */ overlay[local?] module; @@ -257,11 +254,39 @@ private predicate constantCollectionContainsCheck(Guard g, Expr e, boolean branc } /** - * A comparison against a list of compile-time constants, sanitizing taint by - * restricting to a set of known values. + * Holds if the guard `g` ensures that the string expression `e` equals a compile-time constant + * upon evaluating to `branch`. + */ +private predicate constantStringEqualityCheck(Guard g, Expr e, boolean branch) { + exists(MethodCall comparison, Expr constant | + g = comparison and + comparison.getMethod().getDeclaringType() instanceof TypeString and + comparison.getMethod().hasName(["equals", "equalsIgnoreCase"]) and + branch = true and + ( + e = comparison.getQualifier() and constant = comparison.getArgument(0) + or + e = comparison.getArgument(0) and constant = comparison.getQualifier() + ) and + exists(constant.(CompileTimeConstantExpr).getStringValue()) + ) +} + +/** + * Holds if the guard `g` ensures that the expression `e` is one of a set of compile-time constants + * upon evaluating to `branch`. + */ +predicate constantValueGuardChecks(Guard g, Expr e, boolean branch) { + constantStringEqualityCheck(g, e, branch) + or + constantCollectionContainsCheck(g, e, branch) +} + +/** + * A check against compile-time constants, sanitizing taint by restricting a value to a known set. */ -private class ListOfConstantsComparisonSanitizerGuard extends TaintTracking::DefaultTaintSanitizer { - ListOfConstantsComparisonSanitizerGuard() { - this = DataFlow::BarrierGuard::getABarrierNode() +private class ConstantValueSanitizerGuard extends TaintTracking::DefaultTaintSanitizer { + ConstantValueSanitizerGuard() { + this = DataFlow::BarrierGuard::getABarrierNode() } } diff --git a/java/ql/lib/semmle/code/java/security/RequestForgery.qll b/java/ql/lib/semmle/code/java/security/RequestForgery.qll index e1c77a721535..d280d270c7c3 100644 --- a/java/ql/lib/semmle/code/java/security/RequestForgery.qll +++ b/java/ql/lib/semmle/code/java/security/RequestForgery.qll @@ -14,6 +14,7 @@ private import semmle.code.java.controlflow.Guards private import semmle.code.java.dataflow.StringPrefixes private import semmle.code.java.dataflow.ExternalFlow private import semmle.code.java.dataflow.SSA +private import semmle.code.java.security.ListOfConstantsSanitizer private import semmle.code.java.security.Sanitizers /** @@ -149,18 +150,8 @@ private predicate isHostComparisonSanitizer(Guard guard, Expr e, boolean branch) ) ) or - exists(MethodCall comparison, Expr checked, Expr allowed | - guard = comparison and - comparison.getMethod().getDeclaringType() instanceof TypeString and - comparison.getMethod().hasName(["equals", "equalsIgnoreCase"]) and - branch = true and - ( - checked = comparison.getQualifier() and allowed = comparison.getArgument(0) - or - checked = comparison.getArgument(0) and allowed = comparison.getQualifier() - ) and - // New cases require a fixed allowlist entry, not another untrusted value. - exists(allowed.(CompileTimeConstantExpr).getStringValue()) and + exists(Expr checked | + constantValueGuardChecks(guard, checked, branch) and e = getCheckedHost(checked).getQualifier() ) } diff --git a/java/ql/test/library-tests/listofconstants/A.java b/java/ql/test/library-tests/listofconstants/A.java index 74129692d71d..3aea4a2495c9 100644 --- a/java/ql/test/library-tests/listofconstants/A.java +++ b/java/ql/test/library-tests/listofconstants/A.java @@ -32,4 +32,28 @@ private void testAdd(String inp) { sink(inp); } } + + private void checkEquals(String input) { + if ("AA".equals(input)) { + sink(input); + } + } + + private void checkEqualsIgnoreCase(String input) { + if (input.equalsIgnoreCase("AA")) { + sink(input); + } + } + + private void checkWrongBranch(String input) { + if (!"AA".equals(input)) { + sink(input); + } + } + + private void checkUntrustedValue(String input, String allowed) { + if (allowed.equals(input)) { + sink(input); + } + } } diff --git a/java/ql/test/library-tests/listofconstants/test.expected b/java/ql/test/library-tests/listofconstants/test.expected index 203d64cd46dd..89b555b41179 100644 --- a/java/ql/test/library-tests/listofconstants/test.expected +++ b/java/ql/test/library-tests/listofconstants/test.expected @@ -1,3 +1,5 @@ | A.java:12:12:12:20 | separator | | A.java:23:12:23:17 | schema | | A.java:32:12:32:14 | inp | +| A.java:38:12:38:16 | input | +| A.java:44:12:44:16 | input | diff --git a/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java b/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java index edc5638bec26..09575c19098c 100644 --- a/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java +++ b/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java @@ -1,5 +1,6 @@ import java.net.URI; import java.util.Arrays; +import java.util.Set; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; @@ -203,6 +204,30 @@ public void constantAllowlistField(HttpServletRequest request, HttpServletRespon } } + public void constantCollection(HttpServletRequest request, HttpServletResponse response) throws Exception { + URI uri = new URI(request.getParameter("url")); + String host = uri.getHost(); + if (Set.of("example.com", "example.org").contains(host)) { + response.sendRedirect(uri.toString()); + } + } + + public void normalizedConstantCollection(HttpServletRequest request, HttpServletResponse response) throws Exception { + URI uri = new URI(request.getParameter("url")); + String host = uri.getHost(); + if (Set.of("EXAMPLE.COM", "EXAMPLE.ORG").contains(host.toUpperCase())) { + response.sendRedirect(uri.toString()); + } + } + + public void untrustedCollection(HttpServletRequest request, HttpServletResponse response) throws Exception { + URI uri = new URI(request.getParameter("url")); // $ Source + String host = uri.getHost(); + if (Set.of(request.getParameter("allowed")).contains(host)) { + response.sendRedirect(uri.toString()); // $ Alert + } + } + public void orBypass(HttpServletRequest request, HttpServletResponse response, boolean skip) throws Exception { URI uri = new URI(request.getParameter("url")); // $ Source String host = uri.getHost(); diff --git a/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected b/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected index 7a16ee15c617..b7863b009357 100644 --- a/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected +++ b/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected @@ -1,21 +1,22 @@ #select -| HostComparisonRedirect.java:9:31:9:44 | toString(...) | HostComparisonRedirect.java:8:27:8:53 | getParameter(...) : String | HostComparisonRedirect.java:9:31:9:44 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:8:27:8:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:52:35:52:48 | toString(...) | HostComparisonRedirect.java:50:27:50:53 | getParameter(...) : String | HostComparisonRedirect.java:52:35:52:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:50:27:50:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:59:35:59:48 | toString(...) | HostComparisonRedirect.java:57:27:57:53 | getParameter(...) : String | HostComparisonRedirect.java:59:35:59:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:57:27:57:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:70:35:70:48 | toString(...) | HostComparisonRedirect.java:68:27:68:53 | getParameter(...) : String | HostComparisonRedirect.java:70:35:70:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:68:27:68:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:86:35:86:48 | toString(...) | HostComparisonRedirect.java:84:27:84:53 | getParameter(...) : String | HostComparisonRedirect.java:86:35:86:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:84:27:84:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:94:35:94:49 | toString(...) | HostComparisonRedirect.java:92:28:92:54 | getParameter(...) : String | HostComparisonRedirect.java:94:35:94:49 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:92:28:92:54 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:105:35:105:48 | toString(...) | HostComparisonRedirect.java:103:27:103:53 | getParameter(...) : String | HostComparisonRedirect.java:105:35:105:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:103:27:103:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:113:35:113:49 | toString(...) | HostComparisonRedirect.java:111:28:111:54 | getParameter(...) : String | HostComparisonRedirect.java:113:35:113:49 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:111:28:111:54 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:122:35:122:48 | toString(...) | HostComparisonRedirect.java:118:27:118:53 | getParameter(...) : String | HostComparisonRedirect.java:122:35:122:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:118:27:118:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:133:35:133:48 | toString(...) | HostComparisonRedirect.java:127:27:127:53 | getParameter(...) : String | HostComparisonRedirect.java:133:35:133:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:127:27:127:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:142:35:142:48 | toString(...) | HostComparisonRedirect.java:140:23:140:49 | getParameter(...) : String | HostComparisonRedirect.java:142:35:142:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:140:23:140:49 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:149:35:149:48 | toString(...) | HostComparisonRedirect.java:147:27:147:53 | getParameter(...) : String | HostComparisonRedirect.java:149:35:149:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:147:27:147:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:156:35:156:48 | toString(...) | HostComparisonRedirect.java:154:27:154:53 | getParameter(...) : String | HostComparisonRedirect.java:156:35:156:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:154:27:154:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:184:39:184:52 | toString(...) | HostComparisonRedirect.java:177:27:177:53 | getParameter(...) : String | HostComparisonRedirect.java:184:39:184:52 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:177:27:177:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:210:35:210:48 | toString(...) | HostComparisonRedirect.java:207:27:207:53 | getParameter(...) : String | HostComparisonRedirect.java:210:35:210:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:207:27:207:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:236:35:236:48 | toString(...) | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | HostComparisonRedirect.java:236:35:236:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:245:35:245:48 | toString(...) | HostComparisonRedirect.java:241:27:241:53 | getParameter(...) : String | HostComparisonRedirect.java:245:35:245:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:241:27:241:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:10:31:10:44 | toString(...) | HostComparisonRedirect.java:9:27:9:53 | getParameter(...) : String | HostComparisonRedirect.java:10:31:10:44 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:9:27:9:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:53:35:53:48 | toString(...) | HostComparisonRedirect.java:51:27:51:53 | getParameter(...) : String | HostComparisonRedirect.java:53:35:53:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:51:27:51:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:60:35:60:48 | toString(...) | HostComparisonRedirect.java:58:27:58:53 | getParameter(...) : String | HostComparisonRedirect.java:60:35:60:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:58:27:58:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:71:35:71:48 | toString(...) | HostComparisonRedirect.java:69:27:69:53 | getParameter(...) : String | HostComparisonRedirect.java:71:35:71:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:69:27:69:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:87:35:87:48 | toString(...) | HostComparisonRedirect.java:85:27:85:53 | getParameter(...) : String | HostComparisonRedirect.java:87:35:87:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:85:27:85:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:95:35:95:49 | toString(...) | HostComparisonRedirect.java:93:28:93:54 | getParameter(...) : String | HostComparisonRedirect.java:95:35:95:49 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:93:28:93:54 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:106:35:106:48 | toString(...) | HostComparisonRedirect.java:104:27:104:53 | getParameter(...) : String | HostComparisonRedirect.java:106:35:106:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:104:27:104:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:114:35:114:49 | toString(...) | HostComparisonRedirect.java:112:28:112:54 | getParameter(...) : String | HostComparisonRedirect.java:114:35:114:49 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:112:28:112:54 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:123:35:123:48 | toString(...) | HostComparisonRedirect.java:119:27:119:53 | getParameter(...) : String | HostComparisonRedirect.java:123:35:123:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:119:27:119:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:134:35:134:48 | toString(...) | HostComparisonRedirect.java:128:27:128:53 | getParameter(...) : String | HostComparisonRedirect.java:134:35:134:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:128:27:128:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:143:35:143:48 | toString(...) | HostComparisonRedirect.java:141:23:141:49 | getParameter(...) : String | HostComparisonRedirect.java:143:35:143:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:141:23:141:49 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:150:35:150:48 | toString(...) | HostComparisonRedirect.java:148:27:148:53 | getParameter(...) : String | HostComparisonRedirect.java:150:35:150:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:148:27:148:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:157:35:157:48 | toString(...) | HostComparisonRedirect.java:155:27:155:53 | getParameter(...) : String | HostComparisonRedirect.java:157:35:157:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:155:27:155:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:185:39:185:52 | toString(...) | HostComparisonRedirect.java:178:27:178:53 | getParameter(...) : String | HostComparisonRedirect.java:185:39:185:52 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:178:27:178:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:227:35:227:48 | toString(...) | HostComparisonRedirect.java:224:27:224:53 | getParameter(...) : String | HostComparisonRedirect.java:227:35:227:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:224:27:224:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:235:35:235:48 | toString(...) | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | HostComparisonRedirect.java:235:35:235:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:261:35:261:48 | toString(...) | HostComparisonRedirect.java:257:27:257:53 | getParameter(...) : String | HostComparisonRedirect.java:261:35:261:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:257:27:257:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:270:35:270:48 | toString(...) | HostComparisonRedirect.java:266:27:266:53 | getParameter(...) : String | HostComparisonRedirect.java:270:35:270:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:266:27:266:53 | getParameter(...) | user-provided value | | MicronautUrlRedirect.java:14:38:14:55 | create(...) | MicronautUrlRedirect.java:12:39:12:63 | target : String | MicronautUrlRedirect.java:14:38:14:55 | create(...) | Untrusted URL redirection depends on a $@. | MicronautUrlRedirect.java:12:39:12:63 | target | user-provided value | | MicronautUrlRedirect.java:28:47:28:64 | create(...) | MicronautUrlRedirect.java:27:45:27:69 | target : String | MicronautUrlRedirect.java:28:47:28:64 | create(...) | Untrusted URL redirection depends on a $@. | MicronautUrlRedirect.java:27:45:27:69 | target | user-provided value | | MicronautUrlRedirect.java:33:38:33:55 | create(...) | MicronautUrlRedirect.java:32:44:32:68 | target : String | MicronautUrlRedirect.java:33:38:33:55 | create(...) | Untrusted URL redirection depends on a $@. | MicronautUrlRedirect.java:32:44:32:68 | target | user-provided value | @@ -27,57 +28,60 @@ | UrlRedirect.java:42:43:42:72 | getParameter(...) | UrlRedirect.java:42:43:42:72 | getParameter(...) | UrlRedirect.java:42:43:42:72 | getParameter(...) | Untrusted URL redirection depends on a $@. | UrlRedirect.java:42:43:42:72 | getParameter(...) | user-provided value | | mad/Test.java:14:22:14:38 | (...)... | mad/Test.java:9:16:9:41 | getParameter(...) : String | mad/Test.java:14:22:14:38 | (...)... | Untrusted URL redirection depends on a $@. | mad/Test.java:9:16:9:41 | getParameter(...) | user-provided value | edges -| HostComparisonRedirect.java:8:19:8:54 | new URI(...) : URI | HostComparisonRedirect.java:9:31:9:33 | uri : URI | provenance | | -| HostComparisonRedirect.java:8:27:8:53 | getParameter(...) : String | HostComparisonRedirect.java:8:19:8:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:9:31:9:33 | uri : URI | HostComparisonRedirect.java:9:31:9:44 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:50:19:50:54 | new URI(...) : URI | HostComparisonRedirect.java:52:35:52:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:50:27:50:53 | getParameter(...) : String | HostComparisonRedirect.java:50:19:50:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:52:35:52:37 | uri : URI | HostComparisonRedirect.java:52:35:52:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:57:19:57:54 | new URI(...) : URI | HostComparisonRedirect.java:59:35:59:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:57:27:57:53 | getParameter(...) : String | HostComparisonRedirect.java:57:19:57:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:59:35:59:37 | uri : URI | HostComparisonRedirect.java:59:35:59:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:68:19:68:54 | new URI(...) : URI | HostComparisonRedirect.java:70:35:70:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:68:27:68:53 | getParameter(...) : String | HostComparisonRedirect.java:68:19:68:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:70:35:70:37 | uri : URI | HostComparisonRedirect.java:70:35:70:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:84:19:84:54 | new URI(...) : URI | HostComparisonRedirect.java:86:35:86:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:84:27:84:53 | getParameter(...) : String | HostComparisonRedirect.java:84:19:84:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:86:35:86:37 | uri : URI | HostComparisonRedirect.java:86:35:86:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:92:20:92:55 | new URI(...) : URI | HostComparisonRedirect.java:94:35:94:38 | used : URI | provenance | | -| HostComparisonRedirect.java:92:28:92:54 | getParameter(...) : String | HostComparisonRedirect.java:92:20:92:55 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:94:35:94:38 | used : URI | HostComparisonRedirect.java:94:35:94:49 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:103:19:103:54 | new URI(...) : URI | HostComparisonRedirect.java:105:35:105:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:103:27:103:53 | getParameter(...) : String | HostComparisonRedirect.java:103:19:103:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:105:35:105:37 | uri : URI | HostComparisonRedirect.java:105:35:105:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:111:20:111:55 | new URI(...) : URI | HostComparisonRedirect.java:113:35:113:38 | used : URI | provenance | | -| HostComparisonRedirect.java:111:28:111:54 | getParameter(...) : String | HostComparisonRedirect.java:111:20:111:55 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:113:35:113:38 | used : URI | HostComparisonRedirect.java:113:35:113:49 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:118:19:118:54 | new URI(...) : URI | HostComparisonRedirect.java:122:35:122:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:118:27:118:53 | getParameter(...) : String | HostComparisonRedirect.java:118:19:118:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:122:35:122:37 | uri : URI | HostComparisonRedirect.java:122:35:122:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:127:19:127:54 | new URI(...) : URI | HostComparisonRedirect.java:133:35:133:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:127:27:127:53 | getParameter(...) : String | HostComparisonRedirect.java:127:19:127:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:133:35:133:37 | uri : URI | HostComparisonRedirect.java:133:35:133:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:140:15:140:50 | new URI(...) : URI | HostComparisonRedirect.java:142:35:142:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:140:23:140:49 | getParameter(...) : String | HostComparisonRedirect.java:140:15:140:50 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:142:35:142:37 | uri : URI | HostComparisonRedirect.java:142:35:142:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:147:19:147:54 | new URI(...) : URI | HostComparisonRedirect.java:149:35:149:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:147:27:147:53 | getParameter(...) : String | HostComparisonRedirect.java:147:19:147:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:149:35:149:37 | uri : URI | HostComparisonRedirect.java:149:35:149:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:154:19:154:54 | new URI(...) : URI | HostComparisonRedirect.java:156:35:156:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:154:27:154:53 | getParameter(...) : String | HostComparisonRedirect.java:154:19:154:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:156:35:156:37 | uri : URI | HostComparisonRedirect.java:156:35:156:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:177:19:177:54 | new URI(...) : URI | HostComparisonRedirect.java:184:39:184:41 | uri : URI | provenance | | -| HostComparisonRedirect.java:177:27:177:53 | getParameter(...) : String | HostComparisonRedirect.java:177:19:177:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:184:39:184:41 | uri : URI | HostComparisonRedirect.java:184:39:184:52 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:207:19:207:54 | new URI(...) : URI | HostComparisonRedirect.java:210:35:210:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:207:27:207:53 | getParameter(...) : String | HostComparisonRedirect.java:207:19:207:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:210:35:210:37 | uri : URI | HostComparisonRedirect.java:210:35:210:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | HostComparisonRedirect.java:236:35:236:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:9:19:9:54 | new URI(...) : URI | HostComparisonRedirect.java:10:31:10:33 | uri : URI | provenance | | +| HostComparisonRedirect.java:9:27:9:53 | getParameter(...) : String | HostComparisonRedirect.java:9:19:9:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:10:31:10:33 | uri : URI | HostComparisonRedirect.java:10:31:10:44 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:51:19:51:54 | new URI(...) : URI | HostComparisonRedirect.java:53:35:53:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:51:27:51:53 | getParameter(...) : String | HostComparisonRedirect.java:51:19:51:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:53:35:53:37 | uri : URI | HostComparisonRedirect.java:53:35:53:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:58:19:58:54 | new URI(...) : URI | HostComparisonRedirect.java:60:35:60:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:58:27:58:53 | getParameter(...) : String | HostComparisonRedirect.java:58:19:58:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:60:35:60:37 | uri : URI | HostComparisonRedirect.java:60:35:60:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:69:19:69:54 | new URI(...) : URI | HostComparisonRedirect.java:71:35:71:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:69:27:69:53 | getParameter(...) : String | HostComparisonRedirect.java:69:19:69:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:71:35:71:37 | uri : URI | HostComparisonRedirect.java:71:35:71:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:85:19:85:54 | new URI(...) : URI | HostComparisonRedirect.java:87:35:87:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:85:27:85:53 | getParameter(...) : String | HostComparisonRedirect.java:85:19:85:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:87:35:87:37 | uri : URI | HostComparisonRedirect.java:87:35:87:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:93:20:93:55 | new URI(...) : URI | HostComparisonRedirect.java:95:35:95:38 | used : URI | provenance | | +| HostComparisonRedirect.java:93:28:93:54 | getParameter(...) : String | HostComparisonRedirect.java:93:20:93:55 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:95:35:95:38 | used : URI | HostComparisonRedirect.java:95:35:95:49 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:104:19:104:54 | new URI(...) : URI | HostComparisonRedirect.java:106:35:106:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:104:27:104:53 | getParameter(...) : String | HostComparisonRedirect.java:104:19:104:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:106:35:106:37 | uri : URI | HostComparisonRedirect.java:106:35:106:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:112:20:112:55 | new URI(...) : URI | HostComparisonRedirect.java:114:35:114:38 | used : URI | provenance | | +| HostComparisonRedirect.java:112:28:112:54 | getParameter(...) : String | HostComparisonRedirect.java:112:20:112:55 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:114:35:114:38 | used : URI | HostComparisonRedirect.java:114:35:114:49 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:119:19:119:54 | new URI(...) : URI | HostComparisonRedirect.java:123:35:123:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:119:27:119:53 | getParameter(...) : String | HostComparisonRedirect.java:119:19:119:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:123:35:123:37 | uri : URI | HostComparisonRedirect.java:123:35:123:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:128:19:128:54 | new URI(...) : URI | HostComparisonRedirect.java:134:35:134:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:128:27:128:53 | getParameter(...) : String | HostComparisonRedirect.java:128:19:128:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:134:35:134:37 | uri : URI | HostComparisonRedirect.java:134:35:134:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:141:15:141:50 | new URI(...) : URI | HostComparisonRedirect.java:143:35:143:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:141:23:141:49 | getParameter(...) : String | HostComparisonRedirect.java:141:15:141:50 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:143:35:143:37 | uri : URI | HostComparisonRedirect.java:143:35:143:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:148:19:148:54 | new URI(...) : URI | HostComparisonRedirect.java:150:35:150:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:148:27:148:53 | getParameter(...) : String | HostComparisonRedirect.java:148:19:148:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:150:35:150:37 | uri : URI | HostComparisonRedirect.java:150:35:150:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:155:19:155:54 | new URI(...) : URI | HostComparisonRedirect.java:157:35:157:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:155:27:155:53 | getParameter(...) : String | HostComparisonRedirect.java:155:19:155:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:157:35:157:37 | uri : URI | HostComparisonRedirect.java:157:35:157:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:178:19:178:54 | new URI(...) : URI | HostComparisonRedirect.java:185:39:185:41 | uri : URI | provenance | | +| HostComparisonRedirect.java:178:27:178:53 | getParameter(...) : String | HostComparisonRedirect.java:178:19:178:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:185:39:185:41 | uri : URI | HostComparisonRedirect.java:185:39:185:52 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:224:19:224:54 | new URI(...) : URI | HostComparisonRedirect.java:227:35:227:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:224:27:224:53 | getParameter(...) : String | HostComparisonRedirect.java:224:19:224:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:227:35:227:37 | uri : URI | HostComparisonRedirect.java:227:35:227:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | HostComparisonRedirect.java:235:35:235:37 | uri : URI | provenance | | | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:236:35:236:37 | uri : URI | HostComparisonRedirect.java:236:35:236:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:241:19:241:54 | new URI(...) : URI | HostComparisonRedirect.java:245:35:245:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:241:27:241:53 | getParameter(...) : String | HostComparisonRedirect.java:241:19:241:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:245:35:245:37 | uri : URI | HostComparisonRedirect.java:245:35:245:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:235:35:235:37 | uri : URI | HostComparisonRedirect.java:235:35:235:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:257:19:257:54 | new URI(...) : URI | HostComparisonRedirect.java:261:35:261:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:257:27:257:53 | getParameter(...) : String | HostComparisonRedirect.java:257:19:257:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:261:35:261:37 | uri : URI | HostComparisonRedirect.java:261:35:261:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:266:19:266:54 | new URI(...) : URI | HostComparisonRedirect.java:270:35:270:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:266:27:266:53 | getParameter(...) : String | HostComparisonRedirect.java:266:19:266:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:270:35:270:37 | uri : URI | HostComparisonRedirect.java:270:35:270:48 | toString(...) | provenance | MaD:10 | | MicronautUrlRedirect.java:12:39:12:63 | target : String | MicronautUrlRedirect.java:14:49:14:54 | target : String | provenance | | | MicronautUrlRedirect.java:14:49:14:54 | target : String | MicronautUrlRedirect.java:14:38:14:55 | create(...) | provenance | MaD:9 Sink:MaD:2 | | MicronautUrlRedirect.java:27:45:27:69 | target : String | MicronautUrlRedirect.java:28:58:28:63 | target : String | provenance | | @@ -104,74 +108,78 @@ models | 9 | Summary: java.net; URI; false; create; ; ; Argument[0]; ReturnValue; taint; manual | | 10 | Summary: java.net; URI; false; toString; ; ; Argument[this]; ReturnValue; taint; manual | nodes -| HostComparisonRedirect.java:8:19:8:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:8:27:8:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:9:31:9:33 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:9:31:9:44 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:50:19:50:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:50:27:50:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:52:35:52:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:52:35:52:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:57:19:57:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:57:27:57:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:59:35:59:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:59:35:59:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:68:19:68:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:68:27:68:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:70:35:70:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:70:35:70:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:84:19:84:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:84:27:84:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:86:35:86:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:86:35:86:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:92:20:92:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:92:28:92:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:94:35:94:38 | used : URI | semmle.label | used : URI | -| HostComparisonRedirect.java:94:35:94:49 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:103:19:103:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:103:27:103:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:105:35:105:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:105:35:105:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:111:20:111:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:111:28:111:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:113:35:113:38 | used : URI | semmle.label | used : URI | -| HostComparisonRedirect.java:113:35:113:49 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:118:19:118:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:118:27:118:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:122:35:122:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:122:35:122:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:127:19:127:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:127:27:127:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:133:35:133:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:133:35:133:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:140:15:140:50 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:140:23:140:49 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:142:35:142:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:142:35:142:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:147:19:147:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:147:27:147:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:149:35:149:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:149:35:149:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:154:19:154:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:154:27:154:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:156:35:156:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:156:35:156:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:177:19:177:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:177:27:177:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:184:39:184:41 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:184:39:184:52 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:207:19:207:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:207:27:207:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:210:35:210:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:210:35:210:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:9:19:9:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:9:27:9:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:10:31:10:33 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:10:31:10:44 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:51:19:51:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:51:27:51:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:53:35:53:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:53:35:53:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:58:19:58:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:58:27:58:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:60:35:60:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:60:35:60:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:69:19:69:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:69:27:69:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:71:35:71:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:71:35:71:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:85:19:85:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:85:27:85:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:87:35:87:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:87:35:87:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:93:20:93:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:93:28:93:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:95:35:95:38 | used : URI | semmle.label | used : URI | +| HostComparisonRedirect.java:95:35:95:49 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:104:19:104:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:104:27:104:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:106:35:106:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:106:35:106:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:112:20:112:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:112:28:112:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:114:35:114:38 | used : URI | semmle.label | used : URI | +| HostComparisonRedirect.java:114:35:114:49 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:119:19:119:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:119:27:119:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:123:35:123:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:123:35:123:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:128:19:128:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:128:27:128:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:134:35:134:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:134:35:134:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:141:15:141:50 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:141:23:141:49 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:143:35:143:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:143:35:143:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:148:19:148:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:148:27:148:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:150:35:150:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:150:35:150:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:155:19:155:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:155:27:155:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:157:35:157:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:157:35:157:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:178:19:178:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:178:27:178:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:185:39:185:41 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:185:39:185:52 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:224:19:224:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:224:27:224:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:227:35:227:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:227:35:227:48 | toString(...) | semmle.label | toString(...) | | HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:236:35:236:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:236:35:236:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:241:19:241:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:241:27:241:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:245:35:245:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:245:35:245:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:235:35:235:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:235:35:235:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:257:19:257:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:257:27:257:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:261:35:261:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:261:35:261:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:266:19:266:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:266:27:266:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:270:35:270:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:270:35:270:48 | toString(...) | semmle.label | toString(...) | | MicronautUrlRedirect.java:12:39:12:63 | target : String | semmle.label | target : String | | MicronautUrlRedirect.java:14:38:14:55 | create(...) | semmle.label | create(...) | | MicronautUrlRedirect.java:14:49:14:54 | target : String | semmle.label | target : String | diff --git a/java/ql/test/query-tests/security/CWE-918/HostComparison.java b/java/ql/test/query-tests/security/CWE-918/HostComparison.java index 163a74d1d8ae..daecaa4557c7 100644 --- a/java/ql/test/query-tests/security/CWE-918/HostComparison.java +++ b/java/ql/test/query-tests/security/CWE-918/HostComparison.java @@ -1,5 +1,6 @@ import java.net.URI; import java.util.Arrays; +import java.util.Set; import javax.servlet.http.HttpServletRequest; public class HostComparison { @@ -202,6 +203,30 @@ public void constantAllowlistField(HttpServletRequest request) throws Exception } } + public void constantCollection(HttpServletRequest request) throws Exception { + URI uri = new URI(request.getParameter("url")); + String host = uri.getHost(); + if (Set.of("example.com", "example.org").contains(host)) { + uri.toURL().openConnection(); + } + } + + public void normalizedConstantCollection(HttpServletRequest request) throws Exception { + URI uri = new URI(request.getParameter("url")); + String host = uri.getHost(); + if (Set.of("EXAMPLE.COM", "EXAMPLE.ORG").contains(host.toUpperCase())) { + uri.toURL().openConnection(); + } + } + + public void untrustedCollection(HttpServletRequest request) throws Exception { + URI uri = new URI(request.getParameter("url")); // $ Source + String host = uri.getHost(); + if (Set.of(request.getParameter("allowed")).contains(host)) { + uri.toURL().openConnection(); // $ Alert + } + } + public void orBypass(HttpServletRequest request, boolean skip) throws Exception { URI uri = new URI(request.getParameter("url")); // $ Source String host = uri.getHost(); diff --git a/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected b/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected index 3aaa32970e6d..106827a84070 100644 --- a/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected +++ b/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected @@ -210,23 +210,24 @@ | ApacheHttpSSRFVersion5.java:388:44:388:46 | uri | ApacheHttpSSRFVersion5.java:372:30:372:56 | getParameter(...) : String | ApacheHttpSSRFVersion5.java:388:44:388:46 | uri | Potential server-side request forgery due to a $@. | ApacheHttpSSRFVersion5.java:372:30:372:56 | getParameter(...) | user-provided value | | ApacheHttpSSRFVersion5.java:390:24:390:26 | uri | ApacheHttpSSRFVersion5.java:372:30:372:56 | getParameter(...) : String | ApacheHttpSSRFVersion5.java:390:24:390:26 | uri | Potential server-side request forgery due to a $@. | ApacheHttpSSRFVersion5.java:372:30:372:56 | getParameter(...) | user-provided value | | ApacheHttpSSRFVersion5.java:394:24:394:26 | uri | ApacheHttpSSRFVersion5.java:372:30:372:56 | getParameter(...) : String | ApacheHttpSSRFVersion5.java:394:24:394:26 | uri | Potential server-side request forgery due to a $@. | ApacheHttpSSRFVersion5.java:372:30:372:56 | getParameter(...) | user-provided value | -| HostComparison.java:8:9:8:19 | toURL(...) | HostComparison.java:7:27:7:53 | getParameter(...) : String | HostComparison.java:8:9:8:19 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:7:27:7:53 | getParameter(...) | user-provided value | -| HostComparison.java:51:13:51:23 | toURL(...) | HostComparison.java:49:27:49:53 | getParameter(...) : String | HostComparison.java:51:13:51:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:49:27:49:53 | getParameter(...) | user-provided value | -| HostComparison.java:58:13:58:23 | toURL(...) | HostComparison.java:56:27:56:53 | getParameter(...) : String | HostComparison.java:58:13:58:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:56:27:56:53 | getParameter(...) | user-provided value | -| HostComparison.java:69:13:69:23 | toURL(...) | HostComparison.java:67:27:67:53 | getParameter(...) : String | HostComparison.java:69:13:69:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:67:27:67:53 | getParameter(...) | user-provided value | -| HostComparison.java:85:13:85:23 | toURL(...) | HostComparison.java:83:27:83:53 | getParameter(...) : String | HostComparison.java:85:13:85:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:83:27:83:53 | getParameter(...) | user-provided value | -| HostComparison.java:93:13:93:24 | toURL(...) | HostComparison.java:91:28:91:54 | getParameter(...) : String | HostComparison.java:93:13:93:24 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:91:28:91:54 | getParameter(...) | user-provided value | -| HostComparison.java:104:13:104:23 | toURL(...) | HostComparison.java:102:27:102:53 | getParameter(...) : String | HostComparison.java:104:13:104:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:102:27:102:53 | getParameter(...) | user-provided value | -| HostComparison.java:112:13:112:24 | toURL(...) | HostComparison.java:110:28:110:54 | getParameter(...) : String | HostComparison.java:112:13:112:24 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:110:28:110:54 | getParameter(...) | user-provided value | -| HostComparison.java:121:13:121:23 | toURL(...) | HostComparison.java:117:27:117:53 | getParameter(...) : String | HostComparison.java:121:13:121:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:117:27:117:53 | getParameter(...) | user-provided value | -| HostComparison.java:132:13:132:23 | toURL(...) | HostComparison.java:126:27:126:53 | getParameter(...) : String | HostComparison.java:132:13:132:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:126:27:126:53 | getParameter(...) | user-provided value | -| HostComparison.java:141:13:141:23 | toURL(...) | HostComparison.java:139:23:139:49 | getParameter(...) : String | HostComparison.java:141:13:141:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:139:23:139:49 | getParameter(...) | user-provided value | -| HostComparison.java:148:13:148:23 | toURL(...) | HostComparison.java:146:27:146:53 | getParameter(...) : String | HostComparison.java:148:13:148:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:146:27:146:53 | getParameter(...) | user-provided value | -| HostComparison.java:155:13:155:23 | toURL(...) | HostComparison.java:153:27:153:53 | getParameter(...) : String | HostComparison.java:155:13:155:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:153:27:153:53 | getParameter(...) | user-provided value | -| HostComparison.java:183:17:183:27 | toURL(...) | HostComparison.java:176:27:176:53 | getParameter(...) : String | HostComparison.java:183:17:183:27 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:176:27:176:53 | getParameter(...) | user-provided value | -| HostComparison.java:209:13:209:23 | toURL(...) | HostComparison.java:206:27:206:53 | getParameter(...) : String | HostComparison.java:209:13:209:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:206:27:206:53 | getParameter(...) | user-provided value | -| HostComparison.java:235:13:235:23 | toURL(...) | HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:235:13:235:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:231:27:231:53 | getParameter(...) | user-provided value | -| HostComparison.java:244:13:244:23 | toURL(...) | HostComparison.java:240:27:240:53 | getParameter(...) : String | HostComparison.java:244:13:244:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:240:27:240:53 | getParameter(...) | user-provided value | +| HostComparison.java:9:9:9:19 | toURL(...) | HostComparison.java:8:27:8:53 | getParameter(...) : String | HostComparison.java:9:9:9:19 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:8:27:8:53 | getParameter(...) | user-provided value | +| HostComparison.java:52:13:52:23 | toURL(...) | HostComparison.java:50:27:50:53 | getParameter(...) : String | HostComparison.java:52:13:52:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:50:27:50:53 | getParameter(...) | user-provided value | +| HostComparison.java:59:13:59:23 | toURL(...) | HostComparison.java:57:27:57:53 | getParameter(...) : String | HostComparison.java:59:13:59:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:57:27:57:53 | getParameter(...) | user-provided value | +| HostComparison.java:70:13:70:23 | toURL(...) | HostComparison.java:68:27:68:53 | getParameter(...) : String | HostComparison.java:70:13:70:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:68:27:68:53 | getParameter(...) | user-provided value | +| HostComparison.java:86:13:86:23 | toURL(...) | HostComparison.java:84:27:84:53 | getParameter(...) : String | HostComparison.java:86:13:86:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:84:27:84:53 | getParameter(...) | user-provided value | +| HostComparison.java:94:13:94:24 | toURL(...) | HostComparison.java:92:28:92:54 | getParameter(...) : String | HostComparison.java:94:13:94:24 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:92:28:92:54 | getParameter(...) | user-provided value | +| HostComparison.java:105:13:105:23 | toURL(...) | HostComparison.java:103:27:103:53 | getParameter(...) : String | HostComparison.java:105:13:105:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:103:27:103:53 | getParameter(...) | user-provided value | +| HostComparison.java:113:13:113:24 | toURL(...) | HostComparison.java:111:28:111:54 | getParameter(...) : String | HostComparison.java:113:13:113:24 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:111:28:111:54 | getParameter(...) | user-provided value | +| HostComparison.java:122:13:122:23 | toURL(...) | HostComparison.java:118:27:118:53 | getParameter(...) : String | HostComparison.java:122:13:122:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:118:27:118:53 | getParameter(...) | user-provided value | +| HostComparison.java:133:13:133:23 | toURL(...) | HostComparison.java:127:27:127:53 | getParameter(...) : String | HostComparison.java:133:13:133:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:127:27:127:53 | getParameter(...) | user-provided value | +| HostComparison.java:142:13:142:23 | toURL(...) | HostComparison.java:140:23:140:49 | getParameter(...) : String | HostComparison.java:142:13:142:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:140:23:140:49 | getParameter(...) | user-provided value | +| HostComparison.java:149:13:149:23 | toURL(...) | HostComparison.java:147:27:147:53 | getParameter(...) : String | HostComparison.java:149:13:149:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:147:27:147:53 | getParameter(...) | user-provided value | +| HostComparison.java:156:13:156:23 | toURL(...) | HostComparison.java:154:27:154:53 | getParameter(...) : String | HostComparison.java:156:13:156:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:154:27:154:53 | getParameter(...) | user-provided value | +| HostComparison.java:184:17:184:27 | toURL(...) | HostComparison.java:177:27:177:53 | getParameter(...) : String | HostComparison.java:184:17:184:27 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:177:27:177:53 | getParameter(...) | user-provided value | +| HostComparison.java:226:13:226:23 | toURL(...) | HostComparison.java:223:27:223:53 | getParameter(...) : String | HostComparison.java:226:13:226:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:223:27:223:53 | getParameter(...) | user-provided value | +| HostComparison.java:234:13:234:23 | toURL(...) | HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:234:13:234:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:231:27:231:53 | getParameter(...) | user-provided value | +| HostComparison.java:260:13:260:23 | toURL(...) | HostComparison.java:256:27:256:53 | getParameter(...) : String | HostComparison.java:260:13:260:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:256:27:256:53 | getParameter(...) | user-provided value | +| HostComparison.java:269:13:269:23 | toURL(...) | HostComparison.java:265:27:265:53 | getParameter(...) : String | HostComparison.java:269:13:269:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:265:27:265:53 | getParameter(...) | user-provided value | | JakartaWsSSRF.java:15:23:15:25 | url | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) : String | JakartaWsSSRF.java:15:23:15:25 | url | Potential server-side request forgery due to a $@. | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) | user-provided value | | JavaNetHttpSSRF.java:30:32:30:35 | url1 | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | JavaNetHttpSSRF.java:30:32:30:35 | url1 | Potential server-side request forgery due to a $@. | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) | user-provided value | | JavaNetHttpSSRF.java:33:32:33:35 | url1 | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | JavaNetHttpSSRF.java:33:32:33:35 | url1 | Potential server-side request forgery due to a $@. | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) | user-provided value | @@ -733,74 +734,78 @@ edges | ApacheHttpSSRFVersion5.java:376:29:376:50 | new HttpHost(...) : HttpHost | ApacheHttpSSRFVersion5.java:385:50:385:53 | host | provenance | Sink:MaD:206 | | ApacheHttpSSRFVersion5.java:376:29:376:50 | new HttpHost(...) : HttpHost | ApacheHttpSSRFVersion5.java:387:44:387:47 | host | provenance | Sink:MaD:208 | | ApacheHttpSSRFVersion5.java:376:42:376:49 | hostSink : String | ApacheHttpSSRFVersion5.java:376:29:376:50 | new HttpHost(...) : HttpHost | provenance | MaD:315 | -| HostComparison.java:7:19:7:54 | new URI(...) : URI | HostComparison.java:8:9:8:11 | uri : URI | provenance | | -| HostComparison.java:7:27:7:53 | getParameter(...) : String | HostComparison.java:7:19:7:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:7:27:7:53 | getParameter(...) : String | HostComparison.java:7:19:7:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:8:9:8:11 | uri : URI | HostComparison.java:8:9:8:19 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:49:19:49:54 | new URI(...) : URI | HostComparison.java:51:13:51:15 | uri : URI | provenance | | -| HostComparison.java:49:27:49:53 | getParameter(...) : String | HostComparison.java:49:19:49:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:49:27:49:53 | getParameter(...) : String | HostComparison.java:49:19:49:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:51:13:51:15 | uri : URI | HostComparison.java:51:13:51:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:56:19:56:54 | new URI(...) : URI | HostComparison.java:58:13:58:15 | uri : URI | provenance | | -| HostComparison.java:56:27:56:53 | getParameter(...) : String | HostComparison.java:56:19:56:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:56:27:56:53 | getParameter(...) : String | HostComparison.java:56:19:56:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:58:13:58:15 | uri : URI | HostComparison.java:58:13:58:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:67:19:67:54 | new URI(...) : URI | HostComparison.java:69:13:69:15 | uri : URI | provenance | | -| HostComparison.java:67:27:67:53 | getParameter(...) : String | HostComparison.java:67:19:67:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:67:27:67:53 | getParameter(...) : String | HostComparison.java:67:19:67:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:69:13:69:15 | uri : URI | HostComparison.java:69:13:69:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:83:19:83:54 | new URI(...) : URI | HostComparison.java:85:13:85:15 | uri : URI | provenance | | -| HostComparison.java:83:27:83:53 | getParameter(...) : String | HostComparison.java:83:19:83:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:83:27:83:53 | getParameter(...) : String | HostComparison.java:83:19:83:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:85:13:85:15 | uri : URI | HostComparison.java:85:13:85:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:91:20:91:55 | new URI(...) : URI | HostComparison.java:93:13:93:16 | used : URI | provenance | | -| HostComparison.java:91:28:91:54 | getParameter(...) : String | HostComparison.java:91:20:91:55 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:91:28:91:54 | getParameter(...) : String | HostComparison.java:91:20:91:55 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:93:13:93:16 | used : URI | HostComparison.java:93:13:93:24 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:102:19:102:54 | new URI(...) : URI | HostComparison.java:104:13:104:15 | uri : URI | provenance | | -| HostComparison.java:102:27:102:53 | getParameter(...) : String | HostComparison.java:102:19:102:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:102:27:102:53 | getParameter(...) : String | HostComparison.java:102:19:102:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:104:13:104:15 | uri : URI | HostComparison.java:104:13:104:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:110:20:110:55 | new URI(...) : URI | HostComparison.java:112:13:112:16 | used : URI | provenance | | -| HostComparison.java:110:28:110:54 | getParameter(...) : String | HostComparison.java:110:20:110:55 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:110:28:110:54 | getParameter(...) : String | HostComparison.java:110:20:110:55 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:112:13:112:16 | used : URI | HostComparison.java:112:13:112:24 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:117:19:117:54 | new URI(...) : URI | HostComparison.java:121:13:121:15 | uri : URI | provenance | | -| HostComparison.java:117:27:117:53 | getParameter(...) : String | HostComparison.java:117:19:117:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:117:27:117:53 | getParameter(...) : String | HostComparison.java:117:19:117:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:121:13:121:15 | uri : URI | HostComparison.java:121:13:121:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:126:19:126:54 | new URI(...) : URI | HostComparison.java:132:13:132:15 | uri : URI | provenance | | -| HostComparison.java:126:27:126:53 | getParameter(...) : String | HostComparison.java:126:19:126:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:126:27:126:53 | getParameter(...) : String | HostComparison.java:126:19:126:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:132:13:132:15 | uri : URI | HostComparison.java:132:13:132:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:139:15:139:50 | new URI(...) : URI | HostComparison.java:141:13:141:15 | uri : URI | provenance | | -| HostComparison.java:139:23:139:49 | getParameter(...) : String | HostComparison.java:139:15:139:50 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:139:23:139:49 | getParameter(...) : String | HostComparison.java:139:15:139:50 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:141:13:141:15 | uri : URI | HostComparison.java:141:13:141:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:146:19:146:54 | new URI(...) : URI | HostComparison.java:148:13:148:15 | uri : URI | provenance | | -| HostComparison.java:146:27:146:53 | getParameter(...) : String | HostComparison.java:146:19:146:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:146:27:146:53 | getParameter(...) : String | HostComparison.java:146:19:146:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:148:13:148:15 | uri : URI | HostComparison.java:148:13:148:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:153:19:153:54 | new URI(...) : URI | HostComparison.java:155:13:155:15 | uri : URI | provenance | | -| HostComparison.java:153:27:153:53 | getParameter(...) : String | HostComparison.java:153:19:153:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:153:27:153:53 | getParameter(...) : String | HostComparison.java:153:19:153:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:155:13:155:15 | uri : URI | HostComparison.java:155:13:155:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:176:19:176:54 | new URI(...) : URI | HostComparison.java:183:17:183:19 | uri : URI | provenance | | -| HostComparison.java:176:27:176:53 | getParameter(...) : String | HostComparison.java:176:19:176:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:176:27:176:53 | getParameter(...) : String | HostComparison.java:176:19:176:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:183:17:183:19 | uri : URI | HostComparison.java:183:17:183:27 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:206:19:206:54 | new URI(...) : URI | HostComparison.java:209:13:209:15 | uri : URI | provenance | | -| HostComparison.java:206:27:206:53 | getParameter(...) : String | HostComparison.java:206:19:206:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:206:27:206:53 | getParameter(...) : String | HostComparison.java:206:19:206:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:209:13:209:15 | uri : URI | HostComparison.java:209:13:209:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:231:19:231:54 | new URI(...) : URI | HostComparison.java:235:13:235:15 | uri : URI | provenance | | +| HostComparison.java:8:19:8:54 | new URI(...) : URI | HostComparison.java:9:9:9:11 | uri : URI | provenance | | +| HostComparison.java:8:27:8:53 | getParameter(...) : String | HostComparison.java:8:19:8:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:8:27:8:53 | getParameter(...) : String | HostComparison.java:8:19:8:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:9:9:9:11 | uri : URI | HostComparison.java:9:9:9:19 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:50:19:50:54 | new URI(...) : URI | HostComparison.java:52:13:52:15 | uri : URI | provenance | | +| HostComparison.java:50:27:50:53 | getParameter(...) : String | HostComparison.java:50:19:50:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:50:27:50:53 | getParameter(...) : String | HostComparison.java:50:19:50:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:52:13:52:15 | uri : URI | HostComparison.java:52:13:52:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:57:19:57:54 | new URI(...) : URI | HostComparison.java:59:13:59:15 | uri : URI | provenance | | +| HostComparison.java:57:27:57:53 | getParameter(...) : String | HostComparison.java:57:19:57:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:57:27:57:53 | getParameter(...) : String | HostComparison.java:57:19:57:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:59:13:59:15 | uri : URI | HostComparison.java:59:13:59:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:68:19:68:54 | new URI(...) : URI | HostComparison.java:70:13:70:15 | uri : URI | provenance | | +| HostComparison.java:68:27:68:53 | getParameter(...) : String | HostComparison.java:68:19:68:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:68:27:68:53 | getParameter(...) : String | HostComparison.java:68:19:68:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:70:13:70:15 | uri : URI | HostComparison.java:70:13:70:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:84:19:84:54 | new URI(...) : URI | HostComparison.java:86:13:86:15 | uri : URI | provenance | | +| HostComparison.java:84:27:84:53 | getParameter(...) : String | HostComparison.java:84:19:84:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:84:27:84:53 | getParameter(...) : String | HostComparison.java:84:19:84:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:86:13:86:15 | uri : URI | HostComparison.java:86:13:86:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:92:20:92:55 | new URI(...) : URI | HostComparison.java:94:13:94:16 | used : URI | provenance | | +| HostComparison.java:92:28:92:54 | getParameter(...) : String | HostComparison.java:92:20:92:55 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:92:28:92:54 | getParameter(...) : String | HostComparison.java:92:20:92:55 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:94:13:94:16 | used : URI | HostComparison.java:94:13:94:24 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:103:19:103:54 | new URI(...) : URI | HostComparison.java:105:13:105:15 | uri : URI | provenance | | +| HostComparison.java:103:27:103:53 | getParameter(...) : String | HostComparison.java:103:19:103:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:103:27:103:53 | getParameter(...) : String | HostComparison.java:103:19:103:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:105:13:105:15 | uri : URI | HostComparison.java:105:13:105:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:111:20:111:55 | new URI(...) : URI | HostComparison.java:113:13:113:16 | used : URI | provenance | | +| HostComparison.java:111:28:111:54 | getParameter(...) : String | HostComparison.java:111:20:111:55 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:111:28:111:54 | getParameter(...) : String | HostComparison.java:111:20:111:55 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:113:13:113:16 | used : URI | HostComparison.java:113:13:113:24 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:118:19:118:54 | new URI(...) : URI | HostComparison.java:122:13:122:15 | uri : URI | provenance | | +| HostComparison.java:118:27:118:53 | getParameter(...) : String | HostComparison.java:118:19:118:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:118:27:118:53 | getParameter(...) : String | HostComparison.java:118:19:118:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:122:13:122:15 | uri : URI | HostComparison.java:122:13:122:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:127:19:127:54 | new URI(...) : URI | HostComparison.java:133:13:133:15 | uri : URI | provenance | | +| HostComparison.java:127:27:127:53 | getParameter(...) : String | HostComparison.java:127:19:127:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:127:27:127:53 | getParameter(...) : String | HostComparison.java:127:19:127:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:133:13:133:15 | uri : URI | HostComparison.java:133:13:133:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:140:15:140:50 | new URI(...) : URI | HostComparison.java:142:13:142:15 | uri : URI | provenance | | +| HostComparison.java:140:23:140:49 | getParameter(...) : String | HostComparison.java:140:15:140:50 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:140:23:140:49 | getParameter(...) : String | HostComparison.java:140:15:140:50 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:142:13:142:15 | uri : URI | HostComparison.java:142:13:142:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:147:19:147:54 | new URI(...) : URI | HostComparison.java:149:13:149:15 | uri : URI | provenance | | +| HostComparison.java:147:27:147:53 | getParameter(...) : String | HostComparison.java:147:19:147:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:147:27:147:53 | getParameter(...) : String | HostComparison.java:147:19:147:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:149:13:149:15 | uri : URI | HostComparison.java:149:13:149:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:154:19:154:54 | new URI(...) : URI | HostComparison.java:156:13:156:15 | uri : URI | provenance | | +| HostComparison.java:154:27:154:53 | getParameter(...) : String | HostComparison.java:154:19:154:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:154:27:154:53 | getParameter(...) : String | HostComparison.java:154:19:154:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:156:13:156:15 | uri : URI | HostComparison.java:156:13:156:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:177:19:177:54 | new URI(...) : URI | HostComparison.java:184:17:184:19 | uri : URI | provenance | | +| HostComparison.java:177:27:177:53 | getParameter(...) : String | HostComparison.java:177:19:177:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:177:27:177:53 | getParameter(...) : String | HostComparison.java:177:19:177:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:184:17:184:19 | uri : URI | HostComparison.java:184:17:184:27 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:223:19:223:54 | new URI(...) : URI | HostComparison.java:226:13:226:15 | uri : URI | provenance | | +| HostComparison.java:223:27:223:53 | getParameter(...) : String | HostComparison.java:223:19:223:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:223:27:223:53 | getParameter(...) : String | HostComparison.java:223:19:223:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:226:13:226:15 | uri : URI | HostComparison.java:226:13:226:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:231:19:231:54 | new URI(...) : URI | HostComparison.java:234:13:234:15 | uri : URI | provenance | | | HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:231:19:231:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | | HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:231:19:231:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:235:13:235:15 | uri : URI | HostComparison.java:235:13:235:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:240:19:240:54 | new URI(...) : URI | HostComparison.java:244:13:244:15 | uri : URI | provenance | | -| HostComparison.java:240:27:240:53 | getParameter(...) : String | HostComparison.java:240:19:240:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:240:27:240:53 | getParameter(...) : String | HostComparison.java:240:19:240:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:244:13:244:15 | uri : URI | HostComparison.java:244:13:244:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:234:13:234:15 | uri : URI | HostComparison.java:234:13:234:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:256:19:256:54 | new URI(...) : URI | HostComparison.java:260:13:260:15 | uri : URI | provenance | | +| HostComparison.java:256:27:256:53 | getParameter(...) : String | HostComparison.java:256:19:256:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:256:27:256:53 | getParameter(...) : String | HostComparison.java:256:19:256:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:260:13:260:15 | uri : URI | HostComparison.java:260:13:260:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:265:19:265:54 | new URI(...) : URI | HostComparison.java:269:13:269:15 | uri : URI | provenance | | +| HostComparison.java:265:27:265:53 | getParameter(...) : String | HostComparison.java:265:19:265:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:265:27:265:53 | getParameter(...) : String | HostComparison.java:265:19:265:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:269:13:269:15 | uri : URI | HostComparison.java:269:13:269:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) : String | JakartaWsSSRF.java:15:23:15:25 | url | provenance | Src:MaD:292 Sink:MaD:9 | | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | JavaNetHttpSSRF.java:26:31:26:34 | sink : String | provenance | Src:MaD:292 | | JavaNetHttpSSRF.java:26:23:26:35 | new URI(...) : URI | JavaNetHttpSSRF.java:39:59:39:61 | uri | provenance | Sink:MaD:12 | @@ -1859,74 +1864,78 @@ nodes | ApacheHttpSSRFVersion5.java:388:44:388:46 | uri | semmle.label | uri | | ApacheHttpSSRFVersion5.java:390:24:390:26 | uri | semmle.label | uri | | ApacheHttpSSRFVersion5.java:394:24:394:26 | uri | semmle.label | uri | -| HostComparison.java:7:19:7:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:7:27:7:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:8:9:8:11 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:8:9:8:19 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:49:19:49:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:49:27:49:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:51:13:51:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:51:13:51:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:56:19:56:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:56:27:56:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:58:13:58:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:58:13:58:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:67:19:67:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:67:27:67:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:69:13:69:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:69:13:69:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:83:19:83:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:83:27:83:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:85:13:85:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:85:13:85:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:91:20:91:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:91:28:91:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:93:13:93:16 | used : URI | semmle.label | used : URI | -| HostComparison.java:93:13:93:24 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:102:19:102:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:102:27:102:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:104:13:104:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:104:13:104:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:110:20:110:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:110:28:110:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:112:13:112:16 | used : URI | semmle.label | used : URI | -| HostComparison.java:112:13:112:24 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:117:19:117:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:117:27:117:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:121:13:121:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:121:13:121:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:126:19:126:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:126:27:126:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:132:13:132:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:132:13:132:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:139:15:139:50 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:139:23:139:49 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:141:13:141:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:141:13:141:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:146:19:146:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:146:27:146:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:148:13:148:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:148:13:148:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:153:19:153:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:153:27:153:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:155:13:155:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:155:13:155:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:176:19:176:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:176:27:176:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:183:17:183:19 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:183:17:183:27 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:206:19:206:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:206:27:206:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:209:13:209:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:209:13:209:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:8:19:8:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:8:27:8:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:9:9:9:11 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:9:9:9:19 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:50:19:50:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:50:27:50:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:52:13:52:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:52:13:52:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:57:19:57:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:57:27:57:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:59:13:59:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:59:13:59:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:68:19:68:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:68:27:68:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:70:13:70:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:70:13:70:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:84:19:84:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:84:27:84:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:86:13:86:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:86:13:86:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:92:20:92:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:92:28:92:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:94:13:94:16 | used : URI | semmle.label | used : URI | +| HostComparison.java:94:13:94:24 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:103:19:103:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:103:27:103:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:105:13:105:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:105:13:105:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:111:20:111:55 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:111:28:111:54 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:113:13:113:16 | used : URI | semmle.label | used : URI | +| HostComparison.java:113:13:113:24 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:118:19:118:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:118:27:118:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:122:13:122:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:122:13:122:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:127:19:127:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:127:27:127:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:133:13:133:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:133:13:133:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:140:15:140:50 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:140:23:140:49 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:142:13:142:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:142:13:142:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:147:19:147:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:147:27:147:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:149:13:149:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:149:13:149:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:154:19:154:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:154:27:154:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:156:13:156:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:156:13:156:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:177:19:177:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:177:27:177:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:184:17:184:19 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:184:17:184:27 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:223:19:223:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:223:27:223:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:226:13:226:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:226:13:226:23 | toURL(...) | semmle.label | toURL(...) | | HostComparison.java:231:19:231:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | | HostComparison.java:231:27:231:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:235:13:235:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:235:13:235:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:240:19:240:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:240:27:240:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:244:13:244:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:244:13:244:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:234:13:234:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:234:13:234:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:256:19:256:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:256:27:256:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:260:13:260:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:260:13:260:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:265:19:265:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:265:27:265:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:269:13:269:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:269:13:269:23 | toURL(...) | semmle.label | toURL(...) | | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) : String | semmle.label | getParameter(...) : String | | JakartaWsSSRF.java:15:23:15:25 | url | semmle.label | url | | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | From 4dd6cece86e6d89782ac0e946989370ba239f9a0 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Wed, 14 Oct 2026 19:44:48 +0100 Subject: [PATCH 004/140] Rename file ConstantValueSanitizer.qll --- java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll | 2 +- ...{ListOfConstantsSanitizer.qll => ConstantValueSanitizer.qll} | 0 java/ql/lib/semmle/code/java/security/RequestForgery.qll | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename java/ql/lib/semmle/code/java/security/{ListOfConstantsSanitizer.qll => ConstantValueSanitizer.qll} (100%) diff --git a/java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll b/java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll index 6dd8de89e213..615a9640bf07 100644 --- a/java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll +++ b/java/ql/lib/semmle/code/java/dataflow/FlowSteps.qll @@ -30,7 +30,7 @@ private module Frameworks { private import semmle.code.java.frameworks.ThreadLocal private import semmle.code.java.frameworks.ratpack.RatpackExec private import semmle.code.java.frameworks.stapler.Stapler - private import semmle.code.java.security.ListOfConstantsSanitizer + private import semmle.code.java.security.ConstantValueSanitizer private import semmle.code.java.security.PathSanitizer } diff --git a/java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll b/java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll similarity index 100% rename from java/ql/lib/semmle/code/java/security/ListOfConstantsSanitizer.qll rename to java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll diff --git a/java/ql/lib/semmle/code/java/security/RequestForgery.qll b/java/ql/lib/semmle/code/java/security/RequestForgery.qll index d280d270c7c3..5681bfa9f074 100644 --- a/java/ql/lib/semmle/code/java/security/RequestForgery.qll +++ b/java/ql/lib/semmle/code/java/security/RequestForgery.qll @@ -14,7 +14,7 @@ private import semmle.code.java.controlflow.Guards private import semmle.code.java.dataflow.StringPrefixes private import semmle.code.java.dataflow.ExternalFlow private import semmle.code.java.dataflow.SSA -private import semmle.code.java.security.ListOfConstantsSanitizer +private import semmle.code.java.security.ConstantValueSanitizer private import semmle.code.java.security.Sanitizers /** From 6f5965b05d48e439f87af81a4d96829eab95a478 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 15 Oct 2026 09:55:11 +0100 Subject: [PATCH 005/140] Rename test class --- .../CONSISTENCY/typeParametersInScope.expected | 2 +- .../listofconstants/{A.java => ConstantValue.java} | 2 +- .../test/library-tests/listofconstants/test.expected | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) rename java/ql/test/library-tests/listofconstants/{A.java => ConstantValue.java} (97%) diff --git a/java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected b/java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected index 1ea902154a6e..355fb350542e 100644 --- a/java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected +++ b/java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected @@ -1 +1 @@ -| Type A uses out-of-scope type variable E. Note the Java extractor is known to sometimes do this; the Kotlin extractor should not. | +| Type ConstantValue uses out-of-scope type variable E. Note the Java extractor is known to sometimes do this; the Kotlin extractor should not. | diff --git a/java/ql/test/library-tests/listofconstants/A.java b/java/ql/test/library-tests/listofconstants/ConstantValue.java similarity index 97% rename from java/ql/test/library-tests/listofconstants/A.java rename to java/ql/test/library-tests/listofconstants/ConstantValue.java index 3aea4a2495c9..a9c422e64c52 100644 --- a/java/ql/test/library-tests/listofconstants/A.java +++ b/java/ql/test/library-tests/listofconstants/ConstantValue.java @@ -1,6 +1,6 @@ import java.util.*; -public class A { +public class ConstantValue { private static final Set SEPARATORS = Collections.unmodifiableSet( new HashSet<>(Arrays.asList("\t", "\n", ";"))); diff --git a/java/ql/test/library-tests/listofconstants/test.expected b/java/ql/test/library-tests/listofconstants/test.expected index 89b555b41179..f275a0b13b27 100644 --- a/java/ql/test/library-tests/listofconstants/test.expected +++ b/java/ql/test/library-tests/listofconstants/test.expected @@ -1,5 +1,5 @@ -| A.java:12:12:12:20 | separator | -| A.java:23:12:23:17 | schema | -| A.java:32:12:32:14 | inp | -| A.java:38:12:38:16 | input | -| A.java:44:12:44:16 | input | +| ConstantValue.java:12:12:12:20 | separator | +| ConstantValue.java:23:12:23:17 | schema | +| ConstantValue.java:32:12:32:14 | inp | +| ConstantValue.java:38:12:38:16 | input | +| ConstantValue.java:44:12:44:16 | input | From eba0abb767b95c36e48f830b6445be290722dc5f Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 15 Oct 2026 09:55:37 +0100 Subject: [PATCH 006/140] Rename test folder --- .../CONSISTENCY/typeParametersInScope.expected | 0 .../ConstantValue.java | 0 .../{listofconstants => default-taint-sanitizer}/test.expected | 0 .../{listofconstants => default-taint-sanitizer}/test.ql | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename java/ql/test/library-tests/{listofconstants => default-taint-sanitizer}/CONSISTENCY/typeParametersInScope.expected (100%) rename java/ql/test/library-tests/{listofconstants => default-taint-sanitizer}/ConstantValue.java (100%) rename java/ql/test/library-tests/{listofconstants => default-taint-sanitizer}/test.expected (100%) rename java/ql/test/library-tests/{listofconstants => default-taint-sanitizer}/test.ql (100%) diff --git a/java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected b/java/ql/test/library-tests/default-taint-sanitizer/CONSISTENCY/typeParametersInScope.expected similarity index 100% rename from java/ql/test/library-tests/listofconstants/CONSISTENCY/typeParametersInScope.expected rename to java/ql/test/library-tests/default-taint-sanitizer/CONSISTENCY/typeParametersInScope.expected diff --git a/java/ql/test/library-tests/listofconstants/ConstantValue.java b/java/ql/test/library-tests/default-taint-sanitizer/ConstantValue.java similarity index 100% rename from java/ql/test/library-tests/listofconstants/ConstantValue.java rename to java/ql/test/library-tests/default-taint-sanitizer/ConstantValue.java diff --git a/java/ql/test/library-tests/listofconstants/test.expected b/java/ql/test/library-tests/default-taint-sanitizer/test.expected similarity index 100% rename from java/ql/test/library-tests/listofconstants/test.expected rename to java/ql/test/library-tests/default-taint-sanitizer/test.expected diff --git a/java/ql/test/library-tests/listofconstants/test.ql b/java/ql/test/library-tests/default-taint-sanitizer/test.ql similarity index 100% rename from java/ql/test/library-tests/listofconstants/test.ql rename to java/ql/test/library-tests/default-taint-sanitizer/test.ql From ef9a0a1db26abc7e887eef94a1edb903283a4ffe Mon Sep 17 00:00:00 2001 From: Alex Hofer <62021660+Alex-Hofer@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:54:00 +0200 Subject: [PATCH 007/140] JS: Add models for the MCP server SDKs --- .../lib/change-notes/2026-10-03-mcp-models.md | 4 + javascript/ql/lib/ext/fastmcp.model.yml | 15 +++ javascript/ql/lib/ext/mcp.model.yml | 36 ++++++ .../threat-models/sources/fastmcp.ts | 43 +++++++ .../threat-models/sources/mcp.ts | 112 ++++++++++++++++++ 5 files changed, 210 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2026-10-03-mcp-models.md create mode 100644 javascript/ql/lib/ext/fastmcp.model.yml create mode 100644 javascript/ql/lib/ext/mcp.model.yml create mode 100644 javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts create mode 100644 javascript/ql/test/library-tests/threat-models/sources/mcp.ts diff --git a/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md b/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md new file mode 100644 index 000000000000..2db58e4392b4 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added modeling of the `@modelcontextprotocol/sdk` and `@modelcontextprotocol/server` (the official Model Context Protocol SDK) and `fastmcp` npm packages. The arguments of MCP tool, resource and prompt callbacks, the requests that handlers of the low-level `Server` receive, and the request headers and bearer token that the SDK passes to a callback, are now sources of the `remote` threat model. diff --git a/javascript/ql/lib/ext/fastmcp.model.yml b/javascript/ql/lib/ext/fastmcp.model.yml new file mode 100644 index 000000000000..ac2e0288dcd7 --- /dev/null +++ b/javascript/ql/lib/ext/fastmcp.model.yml @@ -0,0 +1,15 @@ +# Sources for MCP servers built with the npm package "fastmcp": the arguments of tools, resource +# templates and prompts. +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: typeModel + data: + - ["fastmcp.FastMCP", "fastmcp", "Member[FastMCP].Instance"] + + - addsTo: + pack: codeql/javascript-all + extensible: sourceModel + data: + - ["fastmcp.FastMCP", "Member[addTool].Argument[0].Member[execute].Parameter[0]", "remote"] + - ["fastmcp.FastMCP", "Member[addResourceTemplate,addPrompt].Argument[0].Member[load].Parameter[0]", "remote"] diff --git a/javascript/ql/lib/ext/mcp.model.yml b/javascript/ql/lib/ext/mcp.model.yml new file mode 100644 index 000000000000..caad119d5153 --- /dev/null +++ b/javascript/ql/lib/ext/mcp.model.yml @@ -0,0 +1,36 @@ +# Sources for MCP servers built with the TypeScript SDK: "@modelcontextprotocol/sdk" (1.x) and +# "@modelcontextprotocol/server" (2.x). The arguments of tool, prompt and resource callbacks are +# input the server receives through MCP, and so are the transport headers and the bearer token the +# SDK hands to a callback. +# +# A package name that contains a dot must be quoted with single quotes; otherwise CodeQL reads the +# text after the first dot as a type name. +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: typeModel + data: + - ["@modelcontextprotocol/sdk.McpServer", "'@modelcontextprotocol/sdk/server/mcp.js'", "Member[McpServer].Instance"] + - ["@modelcontextprotocol/sdk.McpServer", "@modelcontextprotocol/sdk/server/mcp", "Member[McpServer].Instance"] + - ["@modelcontextprotocol/sdk.McpServer", "@modelcontextprotocol/server", "Member[McpServer].Instance"] + - ["@modelcontextprotocol/sdk.McpServer", "'@modelcontextprotocol/sdk/server/mcp.js'.McpServer", ""] + - ["@modelcontextprotocol/sdk.Server", "'@modelcontextprotocol/sdk/server/index.js'", "Member[Server].Instance"] + - ["@modelcontextprotocol/sdk.Server", "@modelcontextprotocol/sdk/server/index", "Member[Server].Instance"] + - ["@modelcontextprotocol/sdk.Server", "@modelcontextprotocol/server", "Member[Server].Instance"] + - ["@modelcontextprotocol/sdk.Server", "@modelcontextprotocol/sdk.McpServer", "Member[server]"] + + - addsTo: + pack: codeql/javascript-all + extensible: sourceModel + data: + # High-level server: the callback is the last argument; its first parameter holds the arguments + - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[0]", "remote"] + - ["@modelcontextprotocol/sdk.McpServer", "Member[resource,registerResource].Argument[1..].Parameter[0,1]", "remote"] + # Transport headers and the bearer token: `extra` in 1.x, `ctx` in 2.x + - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[requestInfo].Member[headers]", "remote"] + - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[authInfo].Member[token]", "remote"] + - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[http].Member[req].Member[headers].Member[get].ReturnValue", "remote"] + - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[http].Member[authInfo].Member[token]", "remote"] + # Low-level server: request handlers receive the request; custom methods (2.x) its params + - ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[1].Parameter[0].Member[params]", "remote"] + - ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[2].Parameter[0]", "remote"] diff --git a/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts b/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts new file mode 100644 index 000000000000..b688e650394f --- /dev/null +++ b/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts @@ -0,0 +1,43 @@ +import { FastMCP } from "fastmcp"; +import { z } from "zod"; + +declare function SINK(value: unknown): void; + +const server = new FastMCP({ name: "test", version: "1.0.0" }); + +server.addTool({ + name: "run", + parameters: z.object({ command: z.string() }), + execute: async (args) => { // $ threat-source=remote + SINK(args.command); // $ hasFlow + return ""; + }, +}); + +server.addResourceTemplate({ + uriTemplate: "note://{name}", + name: "note", + arguments: [{ name: "name" }], + async load({ name }) { // $ threat-source=remote + SINK(name); // $ hasFlow + return { text: "" }; + }, +}); + +server.addPrompt({ + name: "summarize", + arguments: [{ name: "topic" }], + load: async ({ topic }) => { // $ threat-source=remote + SINK(topic); // $ hasFlow + return ""; + }, +}); + +// A static resource takes no arguments: no source. +server.addResource({ + uri: "file:///log", + name: "log", + async load() { + return { text: "" }; + }, +}); diff --git a/javascript/ql/test/library-tests/threat-models/sources/mcp.ts b/javascript/ql/test/library-tests/threat-models/sources/mcp.ts new file mode 100644 index 000000000000..553658f6bfeb --- /dev/null +++ b/javascript/ql/test/library-tests/threat-models/sources/mcp.ts @@ -0,0 +1,112 @@ +import { Server } from "@modelcontextprotocol/sdk/server/index.js"; +import { McpServer, ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js"; +import { McpServer as McpServerNoSuffix } from "@modelcontextprotocol/sdk/server/mcp"; +import { CallToolRequestSchema } from "@modelcontextprotocol/sdk/types.js"; +import { McpServer as McpServerV2, Server as ServerV2 } from "@modelcontextprotocol/server"; +import { z } from "zod"; + +declare function SINK(value: unknown): void; + +const server = new McpServer({ name: "test", version: "1.0.0" }); + +// Tools and prompts: the first parameter of the callback holds the arguments. +server.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote + SINK(command); // $ hasFlow + return { content: [] }; +}); + +server.tool("read", "Read a file", { path: z.string() }, async (args) => { // $ threat-source=remote + SINK(args.path); // $ hasFlow + return { content: [] }; +}); + +server.registerTool("fetch", { inputSchema: { url: z.string() } }, async ({ url }) => { // $ threat-source=remote + SINK(url); // $ hasFlow + return { content: [] }; +}); + +async function namedHandler({ target }: { target: string }) { // $ threat-source=remote + SINK(target); // $ hasFlow + return { content: [] }; +} +server.tool("remove", { target: z.string() }, namedHandler); + +server.prompt("summarize", { topic: z.string() }, ({ topic }) => { // $ threat-source=remote + SINK(topic); // $ hasFlow + return { messages: [] }; +}); + +server.registerPrompt("explain", { argsSchema: { file: z.string() } }, ({ file }) => { // $ threat-source=remote + SINK(file); // $ hasFlow + return { messages: [] }; +}); + +// Resources: the URI and the variables of the template. +server.resource("note", new ResourceTemplate("note://{name}", { list: undefined }), async (uri, { name }) => { // $ threat-source=remote + SINK(uri.href); // $ hasFlow + SINK(name); // $ hasFlow + return { contents: [] }; +}); + +server.registerResource("page", new ResourceTemplate("page://{host}", { list: undefined }), {}, async (uri, variables) => { // $ threat-source=remote + SINK(variables.host); // $ hasFlow + return { contents: [] }; +}); + +// The request headers and the bearer token that the SDK hands to a callback. +server.tool("forward", { id: z.string() }, async (_args, extra) => { // $ threat-source=remote + SINK(extra.requestInfo?.headers["x-target"]); // $ hasFlow threat-source=remote + SINK(extra.authInfo?.token); // $ hasFlow threat-source=remote + SINK(extra.sessionId); + return { content: [] }; +}); + +// The low-level server: handlers receive the request. +const lowLevel = new Server({ name: "test", version: "1.0.0" }, { capabilities: { tools: {} } }); +lowLevel.setRequestHandler(CallToolRequestSchema, async (request) => { + SINK(request.params.arguments?.command); // $ hasFlow threat-source=remote + return { content: [] }; +}); + +server.server.setRequestHandler(CallToolRequestSchema, async (request) => { + SINK(request.params.name); // $ hasFlow threat-source=remote + return { content: [] }; +}); + +// An import path without the file suffix, and a server that arrives as a typed parameter. +new McpServerNoSuffix({ name: "test", version: "1.0.0" }).tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote + SINK(command); // $ hasFlow + return { content: [] }; +}); + +export function register(target: McpServer) { + target.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote + SINK(command); // $ hasFlow + return { content: [] }; + }); +} + +// Version 2 of the SDK: one package, and a context object instead of `extra`. +const serverV2 = new McpServerV2({ name: "test", version: "2.0.0" }); +serverV2.registerTool("fetch", { inputSchema: z.object({ url: z.string() }) }, async ({ url }, ctx) => { // $ threat-source=remote + SINK(url); // $ hasFlow + SINK(ctx.http?.req.headers.get("x-target")); // $ hasFlow threat-source=remote + SINK(ctx.http?.authInfo?.token); // $ hasFlow threat-source=remote + return { content: [] }; +}); + +const lowLevelV2 = new ServerV2({ name: "test", version: "2.0.0" }); +lowLevelV2.setRequestHandler("tools/call", async (request) => { + SINK(request.params.arguments); // $ hasFlow threat-source=remote + return { content: [] }; +}); + +lowLevelV2.setRequestHandler("acme/search", { params: z.object({ query: z.string() }), result: z.object({}) }, async (params) => { // $ threat-source=remote + SINK(params.query); // $ hasFlow + return {}; +}); + +// Not registered with a server: no source. +export function notAHandler(command: string) { + SINK(command); +} From 2341a5813f9126d9e16918328a688d796ce6133a Mon Sep 17 00:00:00 2001 From: Alex Hofer <62021660+Alex-Hofer@users.noreply.github.com> Date: Sat, 3 Oct 2026 19:54:04 +0200 Subject: [PATCH 008/140] JS: Add flow summaries for zod schema parsing --- .../ql/lib/change-notes/2026-10-03-zod.md | 4 +++ javascript/ql/lib/ext/zod.model.yml | 16 ++++++++++ .../threat-models/sources/zod.ts | 30 +++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 javascript/ql/lib/change-notes/2026-10-03-zod.md create mode 100644 javascript/ql/lib/ext/zod.model.yml create mode 100644 javascript/ql/test/library-tests/threat-models/sources/zod.ts diff --git a/javascript/ql/lib/change-notes/2026-10-03-zod.md b/javascript/ql/lib/change-notes/2026-10-03-zod.md new file mode 100644 index 000000000000..5bd9d32240a2 --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-10-03-zod.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Added flow summaries for schema parsing with the `zod` npm package: data now flows from the argument of `parse`, `safeParse`, `parseAsync` and `safeParseAsync` to the parsed result. diff --git a/javascript/ql/lib/ext/zod.model.yml b/javascript/ql/lib/ext/zod.model.yml new file mode 100644 index 000000000000..234a0aec9d01 --- /dev/null +++ b/javascript/ql/lib/ext/zod.model.yml @@ -0,0 +1,16 @@ +extensions: + - addsTo: + pack: codeql/javascript-all + extensible: typeModel + data: + - ["zod", "zod/v3", ""] + - ["zod", "zod/v4", ""] + + - addsTo: + pack: codeql/javascript-all + extensible: summaryModel + data: + - ["zod", "Fuzzy.Member[parse]", "Argument[0]", "ReturnValue", "value"] + - ["zod", "Fuzzy.Member[safeParse]", "Argument[0]", "ReturnValue.Member[data]", "value"] + - ["zod", "Fuzzy.Member[parseAsync]", "Argument[0]", "ReturnValue.Awaited", "value"] + - ["zod", "Fuzzy.Member[safeParseAsync]", "Argument[0]", "ReturnValue.Awaited.Member[data]", "value"] diff --git a/javascript/ql/test/library-tests/threat-models/sources/zod.ts b/javascript/ql/test/library-tests/threat-models/sources/zod.ts new file mode 100644 index 000000000000..fa5177540fbb --- /dev/null +++ b/javascript/ql/test/library-tests/threat-models/sources/zod.ts @@ -0,0 +1,30 @@ +import { z } from "zod"; +import * as z4 from "zod/v4"; + +declare function SINK(value: unknown): void; + +const raw = JSON.parse(process.argv[2]); // $ threat-source=commandargs + +const Options = z.object({ command: z.string() }); +const NextOptions = z4.object({ command: z4.string() }); + +SINK(Options.parse(raw).command); // $ hasFlow +SINK(NextOptions.parse(raw).command); // $ hasFlow + +const checked = Options.safeParse(raw); +if (checked.success) { + SINK(checked.data.command); // $ hasFlow +} +SINK(checked.error); + +async function later() { + SINK((await Options.parseAsync(raw)).command); // $ hasFlow + + const result = await Options.safeParseAsync(raw); + if (result.success) { + SINK(result.data.command); // $ hasFlow + } +} + +// A value that did not come from a source stays clean. +SINK(Options.parse({ command: "ls" }).command); From 3460d05b920d80c3479cc18aaec16475743e89d5 Mon Sep 17 00:00:00 2001 From: Alex Hofer <62021660+Alex-Hofer@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:02:46 +0200 Subject: [PATCH 009/140] JS: Use the ai threat model for MCP handler input Callback arguments and low-level requests are produced by a language model, so they are sources of the opt-in ai threat model added in #22810. Request headers and bearer tokens come from the HTTP client and stay sources of the remote threat model. --- .../lib/change-notes/2026-10-03-mcp-models.md | 2 +- javascript/ql/lib/ext/fastmcp.model.yml | 4 +-- javascript/ql/lib/ext/mcp.model.yml | 8 ++--- .../threat-models/sources/fastmcp.ts | 6 ++-- .../threat-models/sources/mcp.ts | 32 +++++++++---------- 5 files changed, 26 insertions(+), 26 deletions(-) diff --git a/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md b/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md index 2db58e4392b4..ca66290407be 100644 --- a/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md +++ b/javascript/ql/lib/change-notes/2026-10-03-mcp-models.md @@ -1,4 +1,4 @@ --- category: minorAnalysis --- -* Added modeling of the `@modelcontextprotocol/sdk` and `@modelcontextprotocol/server` (the official Model Context Protocol SDK) and `fastmcp` npm packages. The arguments of MCP tool, resource and prompt callbacks, the requests that handlers of the low-level `Server` receive, and the request headers and bearer token that the SDK passes to a callback, are now sources of the `remote` threat model. +* Added modeling of the `@modelcontextprotocol/sdk` and `@modelcontextprotocol/server` (the official Model Context Protocol SDK) and `fastmcp` npm packages. The arguments of MCP tool, resource and prompt callbacks and the requests that handlers of the low-level `Server` receive are now sources of the opt-in `ai` threat model, as their values are produced by a language model. The request headers and the bearer token that the SDK passes to a callback are sources of the `remote` threat model. diff --git a/javascript/ql/lib/ext/fastmcp.model.yml b/javascript/ql/lib/ext/fastmcp.model.yml index ac2e0288dcd7..bc784820c864 100644 --- a/javascript/ql/lib/ext/fastmcp.model.yml +++ b/javascript/ql/lib/ext/fastmcp.model.yml @@ -11,5 +11,5 @@ extensions: pack: codeql/javascript-all extensible: sourceModel data: - - ["fastmcp.FastMCP", "Member[addTool].Argument[0].Member[execute].Parameter[0]", "remote"] - - ["fastmcp.FastMCP", "Member[addResourceTemplate,addPrompt].Argument[0].Member[load].Parameter[0]", "remote"] + - ["fastmcp.FastMCP", "Member[addTool].Argument[0].Member[execute].Parameter[0]", "ai"] + - ["fastmcp.FastMCP", "Member[addResourceTemplate,addPrompt].Argument[0].Member[load].Parameter[0]", "ai"] diff --git a/javascript/ql/lib/ext/mcp.model.yml b/javascript/ql/lib/ext/mcp.model.yml index caad119d5153..30b3bb599947 100644 --- a/javascript/ql/lib/ext/mcp.model.yml +++ b/javascript/ql/lib/ext/mcp.model.yml @@ -24,13 +24,13 @@ extensions: extensible: sourceModel data: # High-level server: the callback is the last argument; its first parameter holds the arguments - - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[0]", "remote"] - - ["@modelcontextprotocol/sdk.McpServer", "Member[resource,registerResource].Argument[1..].Parameter[0,1]", "remote"] + - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[0]", "ai"] + - ["@modelcontextprotocol/sdk.McpServer", "Member[resource,registerResource].Argument[1..].Parameter[0,1]", "ai"] # Transport headers and the bearer token: `extra` in 1.x, `ctx` in 2.x - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[requestInfo].Member[headers]", "remote"] - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[authInfo].Member[token]", "remote"] - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[http].Member[req].Member[headers].Member[get].ReturnValue", "remote"] - ["@modelcontextprotocol/sdk.McpServer", "Member[tool,registerTool,prompt,registerPrompt].Argument[1..].Parameter[1,2].Member[http].Member[authInfo].Member[token]", "remote"] # Low-level server: request handlers receive the request; custom methods (2.x) its params - - ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[1].Parameter[0].Member[params]", "remote"] - - ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[2].Parameter[0]", "remote"] + - ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[1].Parameter[0].Member[params]", "ai"] + - ["@modelcontextprotocol/sdk.Server", "Member[setRequestHandler].Argument[2].Parameter[0]", "ai"] diff --git a/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts b/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts index b688e650394f..82c07f02c4e9 100644 --- a/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts +++ b/javascript/ql/test/library-tests/threat-models/sources/fastmcp.ts @@ -8,7 +8,7 @@ const server = new FastMCP({ name: "test", version: "1.0.0" }); server.addTool({ name: "run", parameters: z.object({ command: z.string() }), - execute: async (args) => { // $ threat-source=remote + execute: async (args) => { // $ threat-source=ai SINK(args.command); // $ hasFlow return ""; }, @@ -18,7 +18,7 @@ server.addResourceTemplate({ uriTemplate: "note://{name}", name: "note", arguments: [{ name: "name" }], - async load({ name }) { // $ threat-source=remote + async load({ name }) { // $ threat-source=ai SINK(name); // $ hasFlow return { text: "" }; }, @@ -27,7 +27,7 @@ server.addResourceTemplate({ server.addPrompt({ name: "summarize", arguments: [{ name: "topic" }], - load: async ({ topic }) => { // $ threat-source=remote + load: async ({ topic }) => { // $ threat-source=ai SINK(topic); // $ hasFlow return ""; }, diff --git a/javascript/ql/test/library-tests/threat-models/sources/mcp.ts b/javascript/ql/test/library-tests/threat-models/sources/mcp.ts index 553658f6bfeb..1e8310d6a2f1 100644 --- a/javascript/ql/test/library-tests/threat-models/sources/mcp.ts +++ b/javascript/ql/test/library-tests/threat-models/sources/mcp.ts @@ -10,51 +10,51 @@ declare function SINK(value: unknown): void; const server = new McpServer({ name: "test", version: "1.0.0" }); // Tools and prompts: the first parameter of the callback holds the arguments. -server.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote +server.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=ai SINK(command); // $ hasFlow return { content: [] }; }); -server.tool("read", "Read a file", { path: z.string() }, async (args) => { // $ threat-source=remote +server.tool("read", "Read a file", { path: z.string() }, async (args) => { // $ threat-source=ai SINK(args.path); // $ hasFlow return { content: [] }; }); -server.registerTool("fetch", { inputSchema: { url: z.string() } }, async ({ url }) => { // $ threat-source=remote +server.registerTool("fetch", { inputSchema: { url: z.string() } }, async ({ url }) => { // $ threat-source=ai SINK(url); // $ hasFlow return { content: [] }; }); -async function namedHandler({ target }: { target: string }) { // $ threat-source=remote +async function namedHandler({ target }: { target: string }) { // $ threat-source=ai SINK(target); // $ hasFlow return { content: [] }; } server.tool("remove", { target: z.string() }, namedHandler); -server.prompt("summarize", { topic: z.string() }, ({ topic }) => { // $ threat-source=remote +server.prompt("summarize", { topic: z.string() }, ({ topic }) => { // $ threat-source=ai SINK(topic); // $ hasFlow return { messages: [] }; }); -server.registerPrompt("explain", { argsSchema: { file: z.string() } }, ({ file }) => { // $ threat-source=remote +server.registerPrompt("explain", { argsSchema: { file: z.string() } }, ({ file }) => { // $ threat-source=ai SINK(file); // $ hasFlow return { messages: [] }; }); // Resources: the URI and the variables of the template. -server.resource("note", new ResourceTemplate("note://{name}", { list: undefined }), async (uri, { name }) => { // $ threat-source=remote +server.resource("note", new ResourceTemplate("note://{name}", { list: undefined }), async (uri, { name }) => { // $ threat-source=ai SINK(uri.href); // $ hasFlow SINK(name); // $ hasFlow return { contents: [] }; }); -server.registerResource("page", new ResourceTemplate("page://{host}", { list: undefined }), {}, async (uri, variables) => { // $ threat-source=remote +server.registerResource("page", new ResourceTemplate("page://{host}", { list: undefined }), {}, async (uri, variables) => { // $ threat-source=ai SINK(variables.host); // $ hasFlow return { contents: [] }; }); // The request headers and the bearer token that the SDK hands to a callback. -server.tool("forward", { id: z.string() }, async (_args, extra) => { // $ threat-source=remote +server.tool("forward", { id: z.string() }, async (_args, extra) => { // $ threat-source=ai SINK(extra.requestInfo?.headers["x-target"]); // $ hasFlow threat-source=remote SINK(extra.authInfo?.token); // $ hasFlow threat-source=remote SINK(extra.sessionId); @@ -64,23 +64,23 @@ server.tool("forward", { id: z.string() }, async (_args, extra) => { // $ threat // The low-level server: handlers receive the request. const lowLevel = new Server({ name: "test", version: "1.0.0" }, { capabilities: { tools: {} } }); lowLevel.setRequestHandler(CallToolRequestSchema, async (request) => { - SINK(request.params.arguments?.command); // $ hasFlow threat-source=remote + SINK(request.params.arguments?.command); // $ hasFlow threat-source=ai return { content: [] }; }); server.server.setRequestHandler(CallToolRequestSchema, async (request) => { - SINK(request.params.name); // $ hasFlow threat-source=remote + SINK(request.params.name); // $ hasFlow threat-source=ai return { content: [] }; }); // An import path without the file suffix, and a server that arrives as a typed parameter. -new McpServerNoSuffix({ name: "test", version: "1.0.0" }).tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote +new McpServerNoSuffix({ name: "test", version: "1.0.0" }).tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=ai SINK(command); // $ hasFlow return { content: [] }; }); export function register(target: McpServer) { - target.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=remote + target.tool("run", { command: z.string() }, async ({ command }) => { // $ threat-source=ai SINK(command); // $ hasFlow return { content: [] }; }); @@ -88,7 +88,7 @@ export function register(target: McpServer) { // Version 2 of the SDK: one package, and a context object instead of `extra`. const serverV2 = new McpServerV2({ name: "test", version: "2.0.0" }); -serverV2.registerTool("fetch", { inputSchema: z.object({ url: z.string() }) }, async ({ url }, ctx) => { // $ threat-source=remote +serverV2.registerTool("fetch", { inputSchema: z.object({ url: z.string() }) }, async ({ url }, ctx) => { // $ threat-source=ai SINK(url); // $ hasFlow SINK(ctx.http?.req.headers.get("x-target")); // $ hasFlow threat-source=remote SINK(ctx.http?.authInfo?.token); // $ hasFlow threat-source=remote @@ -97,11 +97,11 @@ serverV2.registerTool("fetch", { inputSchema: z.object({ url: z.string() }) }, a const lowLevelV2 = new ServerV2({ name: "test", version: "2.0.0" }); lowLevelV2.setRequestHandler("tools/call", async (request) => { - SINK(request.params.arguments); // $ hasFlow threat-source=remote + SINK(request.params.arguments); // $ hasFlow threat-source=ai return { content: [] }; }); -lowLevelV2.setRequestHandler("acme/search", { params: z.object({ query: z.string() }), result: z.object({}) }, async (params) => { // $ threat-source=remote +lowLevelV2.setRequestHandler("acme/search", { params: z.object({ query: z.string() }), result: z.object({}) }, async (params) => { // $ threat-source=ai SINK(params.query); // $ hasFlow return {}; }); From df832a4078c6ec7aab9139b675588a4ace8c72d3 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 16 Oct 2026 20:53:56 +0100 Subject: [PATCH 010/140] Java: Add null host allowlist regression tests Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../semmle/tests/HostComparisonRedirect.java | 16 ++++++++++++++++ .../security/CWE-918/HostComparison.java | 16 ++++++++++++++++ 2 files changed, 32 insertions(+) diff --git a/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java b/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java index 09575c19098c..e9b6cf1d3003 100644 --- a/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java +++ b/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java @@ -220,6 +220,22 @@ public void normalizedConstantCollection(HttpServletRequest request, HttpServlet } } + public void nullContainingCollection(HttpServletRequest request, HttpServletResponse response) throws Exception { + URI uri = new URI(request.getParameter("url")); // $ MISSING: Source + String host = uri.getHost(); + if (Arrays.asList("example.com", null).contains(host)) { + response.sendRedirect(uri.toString()); // $ MISSING: Alert + } + } + + public void nullContainingCollectionWithNonNullHost(HttpServletRequest request, HttpServletResponse response) throws Exception { + URI uri = new URI(request.getParameter("url")); + String host = uri.getHost(); + if (host != null && Arrays.asList("example.com", null).contains(host)) { + response.sendRedirect(uri.toString()); + } + } + public void untrustedCollection(HttpServletRequest request, HttpServletResponse response) throws Exception { URI uri = new URI(request.getParameter("url")); // $ Source String host = uri.getHost(); diff --git a/java/ql/test/query-tests/security/CWE-918/HostComparison.java b/java/ql/test/query-tests/security/CWE-918/HostComparison.java index daecaa4557c7..42dbc077ea8d 100644 --- a/java/ql/test/query-tests/security/CWE-918/HostComparison.java +++ b/java/ql/test/query-tests/security/CWE-918/HostComparison.java @@ -219,6 +219,22 @@ public void normalizedConstantCollection(HttpServletRequest request) throws Exce } } + public void nullContainingCollection(HttpServletRequest request) throws Exception { + URI uri = new URI(request.getParameter("url")); // $ MISSING: Source + String host = uri.getHost(); + if (Arrays.asList("example.com", null).contains(host)) { + uri.toURL().openConnection(); // $ MISSING: Alert + } + } + + public void nullContainingCollectionWithNonNullHost(HttpServletRequest request) throws Exception { + URI uri = new URI(request.getParameter("url")); + String host = uri.getHost(); + if (host != null && Arrays.asList("example.com", null).contains(host)) { + uri.toURL().openConnection(); + } + } + public void untrustedCollection(HttpServletRequest request) throws Exception { URI uri = new URI(request.getParameter("url")); // $ Source String host = uri.getHost(); From a6202787f0e22620e606fdc8845fed9eae14e3c7 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Fri, 16 Oct 2026 20:58:51 +0100 Subject: [PATCH 011/140] Java: Require non-null host allowlist matches Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../java/security/ConstantValueSanitizer.qll | 76 ++++++++++++++++--- .../code/java/security/RequestForgery.qll | 2 +- .../semmle/tests/HostComparisonRedirect.java | 4 +- .../CWE-601/semmle/tests/UrlRedirect.expected | 56 ++++++++------ .../security/CWE-918/HostComparison.java | 4 +- .../security/CWE-918/RequestForgery.expected | 63 ++++++++------- 6 files changed, 140 insertions(+), 65 deletions(-) diff --git a/java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll b/java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll index 480191c1e412..46020585d0e1 100644 --- a/java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll +++ b/java/ql/lib/semmle/code/java/security/ConstantValueSanitizer.qll @@ -7,6 +7,7 @@ private import codeql.typeflow.UniversalFlow as UniversalFlow private import semmle.code.java.Collections private import semmle.code.java.controlflow.Guards private import semmle.code.java.dataflow.internal.BaseSSA as Base +private import semmle.code.java.dataflow.NullGuards private import semmle.code.java.dataflow.TaintTracking private import semmle.code.java.dataflow.TypeFlow private import semmle.code.java.dispatch.VirtualDispatch @@ -154,6 +155,29 @@ private predicate collectionWithPossibleMutation(FlowNode n) { ) } +/** Holds if `n` is a collection constructed by an API that rejects null elements. */ +private predicate nullRejectingCollectionCreation(FlowNode n) { + exists(Call c, Callable tgt | + n.asExpr() = c and + c.getCallee().getSourceDeclaration() = tgt + | + tgt.hasQualifiedName("java.util", ["List", "Set"], ["copyOf", "of"]) + or + tgt.hasQualifiedName("com.google.common.collect", ["ImmutableList", "ImmutableSet"], + ["copyOf", "of"]) + ) +} + +private module NullRejectingCollectionProp implements BaseUniversalFlow::NullaryPropertySig { + predicate hasPropertyBase(FlowNode n) { nullRejectingCollectionCreation(n) } + + predicate barrier = collectionWithPossibleMutation/1; +} + +/** Holds if the given collection cannot contain null. */ +private predicate nullRejectingCollection = + BaseUniversalFlow::FlowNullary::hasProperty/1; + /** * A collection constructor that constructs an empty mutable collection. */ @@ -238,18 +262,23 @@ private MethodCall normalizeCaseCall(Expr arg) { * Holds if the guard `g` ensures that the expression `e` is one of a set of * known constants upon evaluating to `branch`. */ -private predicate constantCollectionContainsCheck(Guard g, Expr e, boolean branch) { - exists(MethodCall mc, Method m, FlowNode n, Expr checked | - g = mc and - mc.getMethod().getSourceDeclaration().overridesOrInstantiates*(m) and +private predicate constantCollectionContainsCall( + MethodCall contains, FlowNode collection, Expr checked +) { + exists(Method m, FlowNode n | + contains.getMethod().getSourceDeclaration().overridesOrInstantiates*(m) and m.hasQualifiedName("java.util", "Collection", "contains") and - n.asExpr() = mc.getQualifier() and + n.asExpr() = contains.getQualifier() and constantCollection(n) and - checked = mc.getAnArgument() and + checked = contains.getAnArgument() and + collection = n + ) +} + +private predicate constantCollectionContainsCheck(Guard g, Expr e, boolean branch) { + exists(Expr checked | checked = e or checked = normalizeCaseCall(e) | + constantCollectionContainsCall(g, _, checked) and branch = true - | - checked = e or - checked = normalizeCaseCall(e) ) } @@ -282,6 +311,35 @@ predicate constantValueGuardChecks(Guard g, Expr e, boolean branch) { constantCollectionContainsCheck(g, e, branch) } +/** + * Holds if `g` ensures that `e` is a non-null member of a set of compile-time constants upon + * evaluating to `branch`. + */ +predicate nonNullConstantValueGuardChecks(Guard g, Expr e, boolean branch) { + constantStringEqualityCheck(g, e, branch) + or + exists(FlowNode collection, Expr checked | + constantCollectionContainsCall(g, collection, checked) and + branch = true + | + checked = e and + ( + nullRejectingCollection(collection) + or + e = clearlyNotNullExpr() + ) + or + checked = normalizeCaseCall(e) and + ( + nullRejectingCollection(collection) + or + checked.(MethodCall).getQualifier() = e + or + e = clearlyNotNullExpr() + ) + ) +} + /** * A check against compile-time constants, sanitizing taint by restricting a value to a known set. */ diff --git a/java/ql/lib/semmle/code/java/security/RequestForgery.qll b/java/ql/lib/semmle/code/java/security/RequestForgery.qll index 5681bfa9f074..3711a93c0530 100644 --- a/java/ql/lib/semmle/code/java/security/RequestForgery.qll +++ b/java/ql/lib/semmle/code/java/security/RequestForgery.qll @@ -151,7 +151,7 @@ private predicate isHostComparisonSanitizer(Guard guard, Expr e, boolean branch) ) or exists(Expr checked | - constantValueGuardChecks(guard, checked, branch) and + nonNullConstantValueGuardChecks(guard, checked, branch) and e = getCheckedHost(checked).getQualifier() ) } diff --git a/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java b/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java index e9b6cf1d3003..2b300e5f1301 100644 --- a/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java +++ b/java/ql/test/query-tests/security/CWE-601/semmle/tests/HostComparisonRedirect.java @@ -221,10 +221,10 @@ public void normalizedConstantCollection(HttpServletRequest request, HttpServlet } public void nullContainingCollection(HttpServletRequest request, HttpServletResponse response) throws Exception { - URI uri = new URI(request.getParameter("url")); // $ MISSING: Source + URI uri = new URI(request.getParameter("url")); // $ Source String host = uri.getHost(); if (Arrays.asList("example.com", null).contains(host)) { - response.sendRedirect(uri.toString()); // $ MISSING: Alert + response.sendRedirect(uri.toString()); // $ Alert } } diff --git a/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected b/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected index b7863b009357..c677916dd36d 100644 --- a/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected +++ b/java/ql/test/query-tests/security/CWE-601/semmle/tests/UrlRedirect.expected @@ -14,9 +14,10 @@ | HostComparisonRedirect.java:157:35:157:48 | toString(...) | HostComparisonRedirect.java:155:27:155:53 | getParameter(...) : String | HostComparisonRedirect.java:157:35:157:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:155:27:155:53 | getParameter(...) | user-provided value | | HostComparisonRedirect.java:185:39:185:52 | toString(...) | HostComparisonRedirect.java:178:27:178:53 | getParameter(...) : String | HostComparisonRedirect.java:185:39:185:52 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:178:27:178:53 | getParameter(...) | user-provided value | | HostComparisonRedirect.java:227:35:227:48 | toString(...) | HostComparisonRedirect.java:224:27:224:53 | getParameter(...) : String | HostComparisonRedirect.java:227:35:227:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:224:27:224:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:235:35:235:48 | toString(...) | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | HostComparisonRedirect.java:235:35:235:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:232:27:232:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:261:35:261:48 | toString(...) | HostComparisonRedirect.java:257:27:257:53 | getParameter(...) : String | HostComparisonRedirect.java:261:35:261:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:257:27:257:53 | getParameter(...) | user-provided value | -| HostComparisonRedirect.java:270:35:270:48 | toString(...) | HostComparisonRedirect.java:266:27:266:53 | getParameter(...) : String | HostComparisonRedirect.java:270:35:270:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:266:27:266:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:243:35:243:48 | toString(...) | HostComparisonRedirect.java:240:27:240:53 | getParameter(...) : String | HostComparisonRedirect.java:243:35:243:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:240:27:240:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:251:35:251:48 | toString(...) | HostComparisonRedirect.java:248:27:248:53 | getParameter(...) : String | HostComparisonRedirect.java:251:35:251:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:248:27:248:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:277:35:277:48 | toString(...) | HostComparisonRedirect.java:273:27:273:53 | getParameter(...) : String | HostComparisonRedirect.java:277:35:277:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:273:27:273:53 | getParameter(...) | user-provided value | +| HostComparisonRedirect.java:286:35:286:48 | toString(...) | HostComparisonRedirect.java:282:27:282:53 | getParameter(...) : String | HostComparisonRedirect.java:286:35:286:48 | toString(...) | Untrusted URL redirection depends on a $@. | HostComparisonRedirect.java:282:27:282:53 | getParameter(...) | user-provided value | | MicronautUrlRedirect.java:14:38:14:55 | create(...) | MicronautUrlRedirect.java:12:39:12:63 | target : String | MicronautUrlRedirect.java:14:38:14:55 | create(...) | Untrusted URL redirection depends on a $@. | MicronautUrlRedirect.java:12:39:12:63 | target | user-provided value | | MicronautUrlRedirect.java:28:47:28:64 | create(...) | MicronautUrlRedirect.java:27:45:27:69 | target : String | MicronautUrlRedirect.java:28:47:28:64 | create(...) | Untrusted URL redirection depends on a $@. | MicronautUrlRedirect.java:27:45:27:69 | target | user-provided value | | MicronautUrlRedirect.java:33:38:33:55 | create(...) | MicronautUrlRedirect.java:32:44:32:68 | target : String | MicronautUrlRedirect.java:33:38:33:55 | create(...) | Untrusted URL redirection depends on a $@. | MicronautUrlRedirect.java:32:44:32:68 | target | user-provided value | @@ -73,15 +74,18 @@ edges | HostComparisonRedirect.java:224:19:224:54 | new URI(...) : URI | HostComparisonRedirect.java:227:35:227:37 | uri : URI | provenance | | | HostComparisonRedirect.java:224:27:224:53 | getParameter(...) : String | HostComparisonRedirect.java:224:19:224:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | | HostComparisonRedirect.java:227:35:227:37 | uri : URI | HostComparisonRedirect.java:227:35:227:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | HostComparisonRedirect.java:235:35:235:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:235:35:235:37 | uri : URI | HostComparisonRedirect.java:235:35:235:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:257:19:257:54 | new URI(...) : URI | HostComparisonRedirect.java:261:35:261:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:257:27:257:53 | getParameter(...) : String | HostComparisonRedirect.java:257:19:257:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:261:35:261:37 | uri : URI | HostComparisonRedirect.java:261:35:261:48 | toString(...) | provenance | MaD:10 | -| HostComparisonRedirect.java:266:19:266:54 | new URI(...) : URI | HostComparisonRedirect.java:270:35:270:37 | uri : URI | provenance | | -| HostComparisonRedirect.java:266:27:266:53 | getParameter(...) : String | HostComparisonRedirect.java:266:19:266:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | -| HostComparisonRedirect.java:270:35:270:37 | uri : URI | HostComparisonRedirect.java:270:35:270:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:240:19:240:54 | new URI(...) : URI | HostComparisonRedirect.java:243:35:243:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:240:27:240:53 | getParameter(...) : String | HostComparisonRedirect.java:240:19:240:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:243:35:243:37 | uri : URI | HostComparisonRedirect.java:243:35:243:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:248:19:248:54 | new URI(...) : URI | HostComparisonRedirect.java:251:35:251:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:248:27:248:53 | getParameter(...) : String | HostComparisonRedirect.java:248:19:248:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:251:35:251:37 | uri : URI | HostComparisonRedirect.java:251:35:251:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:273:19:273:54 | new URI(...) : URI | HostComparisonRedirect.java:277:35:277:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:273:27:273:53 | getParameter(...) : String | HostComparisonRedirect.java:273:19:273:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:277:35:277:37 | uri : URI | HostComparisonRedirect.java:277:35:277:48 | toString(...) | provenance | MaD:10 | +| HostComparisonRedirect.java:282:19:282:54 | new URI(...) : URI | HostComparisonRedirect.java:286:35:286:37 | uri : URI | provenance | | +| HostComparisonRedirect.java:282:27:282:53 | getParameter(...) : String | HostComparisonRedirect.java:282:19:282:54 | new URI(...) : URI | provenance | Src:MaD:6 MaD:8 | +| HostComparisonRedirect.java:286:35:286:37 | uri : URI | HostComparisonRedirect.java:286:35:286:48 | toString(...) | provenance | MaD:10 | | MicronautUrlRedirect.java:12:39:12:63 | target : String | MicronautUrlRedirect.java:14:49:14:54 | target : String | provenance | | | MicronautUrlRedirect.java:14:49:14:54 | target : String | MicronautUrlRedirect.java:14:38:14:55 | create(...) | provenance | MaD:9 Sink:MaD:2 | | MicronautUrlRedirect.java:27:45:27:69 | target : String | MicronautUrlRedirect.java:28:58:28:63 | target : String | provenance | | @@ -168,18 +172,22 @@ nodes | HostComparisonRedirect.java:224:27:224:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | | HostComparisonRedirect.java:227:35:227:37 | uri : URI | semmle.label | uri : URI | | HostComparisonRedirect.java:227:35:227:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:232:19:232:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:232:27:232:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:235:35:235:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:235:35:235:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:257:19:257:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:257:27:257:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:261:35:261:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:261:35:261:48 | toString(...) | semmle.label | toString(...) | -| HostComparisonRedirect.java:266:19:266:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparisonRedirect.java:266:27:266:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparisonRedirect.java:270:35:270:37 | uri : URI | semmle.label | uri : URI | -| HostComparisonRedirect.java:270:35:270:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:240:19:240:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:240:27:240:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:243:35:243:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:243:35:243:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:248:19:248:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:248:27:248:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:251:35:251:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:251:35:251:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:273:19:273:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:273:27:273:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:277:35:277:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:277:35:277:48 | toString(...) | semmle.label | toString(...) | +| HostComparisonRedirect.java:282:19:282:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparisonRedirect.java:282:27:282:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparisonRedirect.java:286:35:286:37 | uri : URI | semmle.label | uri : URI | +| HostComparisonRedirect.java:286:35:286:48 | toString(...) | semmle.label | toString(...) | | MicronautUrlRedirect.java:12:39:12:63 | target : String | semmle.label | target : String | | MicronautUrlRedirect.java:14:38:14:55 | create(...) | semmle.label | create(...) | | MicronautUrlRedirect.java:14:49:14:54 | target : String | semmle.label | target : String | diff --git a/java/ql/test/query-tests/security/CWE-918/HostComparison.java b/java/ql/test/query-tests/security/CWE-918/HostComparison.java index 42dbc077ea8d..143d33599c38 100644 --- a/java/ql/test/query-tests/security/CWE-918/HostComparison.java +++ b/java/ql/test/query-tests/security/CWE-918/HostComparison.java @@ -220,10 +220,10 @@ public void normalizedConstantCollection(HttpServletRequest request) throws Exce } public void nullContainingCollection(HttpServletRequest request) throws Exception { - URI uri = new URI(request.getParameter("url")); // $ MISSING: Source + URI uri = new URI(request.getParameter("url")); // $ Source String host = uri.getHost(); if (Arrays.asList("example.com", null).contains(host)) { - uri.toURL().openConnection(); // $ MISSING: Alert + uri.toURL().openConnection(); // $ Alert } } diff --git a/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected b/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected index 106827a84070..8995e286cef3 100644 --- a/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected +++ b/java/ql/test/query-tests/security/CWE-918/RequestForgery.expected @@ -225,9 +225,10 @@ | HostComparison.java:156:13:156:23 | toURL(...) | HostComparison.java:154:27:154:53 | getParameter(...) : String | HostComparison.java:156:13:156:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:154:27:154:53 | getParameter(...) | user-provided value | | HostComparison.java:184:17:184:27 | toURL(...) | HostComparison.java:177:27:177:53 | getParameter(...) : String | HostComparison.java:184:17:184:27 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:177:27:177:53 | getParameter(...) | user-provided value | | HostComparison.java:226:13:226:23 | toURL(...) | HostComparison.java:223:27:223:53 | getParameter(...) : String | HostComparison.java:226:13:226:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:223:27:223:53 | getParameter(...) | user-provided value | -| HostComparison.java:234:13:234:23 | toURL(...) | HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:234:13:234:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:231:27:231:53 | getParameter(...) | user-provided value | -| HostComparison.java:260:13:260:23 | toURL(...) | HostComparison.java:256:27:256:53 | getParameter(...) : String | HostComparison.java:260:13:260:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:256:27:256:53 | getParameter(...) | user-provided value | -| HostComparison.java:269:13:269:23 | toURL(...) | HostComparison.java:265:27:265:53 | getParameter(...) : String | HostComparison.java:269:13:269:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:265:27:265:53 | getParameter(...) | user-provided value | +| HostComparison.java:242:13:242:23 | toURL(...) | HostComparison.java:239:27:239:53 | getParameter(...) : String | HostComparison.java:242:13:242:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:239:27:239:53 | getParameter(...) | user-provided value | +| HostComparison.java:250:13:250:23 | toURL(...) | HostComparison.java:247:27:247:53 | getParameter(...) : String | HostComparison.java:250:13:250:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:247:27:247:53 | getParameter(...) | user-provided value | +| HostComparison.java:276:13:276:23 | toURL(...) | HostComparison.java:272:27:272:53 | getParameter(...) : String | HostComparison.java:276:13:276:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:272:27:272:53 | getParameter(...) | user-provided value | +| HostComparison.java:285:13:285:23 | toURL(...) | HostComparison.java:281:27:281:53 | getParameter(...) : String | HostComparison.java:285:13:285:23 | toURL(...) | Potential server-side request forgery due to a $@. | HostComparison.java:281:27:281:53 | getParameter(...) | user-provided value | | JakartaWsSSRF.java:15:23:15:25 | url | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) : String | JakartaWsSSRF.java:15:23:15:25 | url | Potential server-side request forgery due to a $@. | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) | user-provided value | | JavaNetHttpSSRF.java:30:32:30:35 | url1 | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | JavaNetHttpSSRF.java:30:32:30:35 | url1 | Potential server-side request forgery due to a $@. | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) | user-provided value | | JavaNetHttpSSRF.java:33:32:33:35 | url1 | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | JavaNetHttpSSRF.java:33:32:33:35 | url1 | Potential server-side request forgery due to a $@. | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) | user-provided value | @@ -794,18 +795,22 @@ edges | HostComparison.java:223:27:223:53 | getParameter(...) : String | HostComparison.java:223:19:223:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | | HostComparison.java:223:27:223:53 | getParameter(...) : String | HostComparison.java:223:19:223:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | | HostComparison.java:226:13:226:15 | uri : URI | HostComparison.java:226:13:226:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:231:19:231:54 | new URI(...) : URI | HostComparison.java:234:13:234:15 | uri : URI | provenance | | -| HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:231:19:231:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:231:27:231:53 | getParameter(...) : String | HostComparison.java:231:19:231:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:234:13:234:15 | uri : URI | HostComparison.java:234:13:234:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:256:19:256:54 | new URI(...) : URI | HostComparison.java:260:13:260:15 | uri : URI | provenance | | -| HostComparison.java:256:27:256:53 | getParameter(...) : String | HostComparison.java:256:19:256:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:256:27:256:53 | getParameter(...) : String | HostComparison.java:256:19:256:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:260:13:260:15 | uri : URI | HostComparison.java:260:13:260:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | -| HostComparison.java:265:19:265:54 | new URI(...) : URI | HostComparison.java:269:13:269:15 | uri : URI | provenance | | -| HostComparison.java:265:27:265:53 | getParameter(...) : String | HostComparison.java:265:19:265:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | -| HostComparison.java:265:27:265:53 | getParameter(...) : String | HostComparison.java:265:19:265:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | -| HostComparison.java:269:13:269:15 | uri : URI | HostComparison.java:269:13:269:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:239:19:239:54 | new URI(...) : URI | HostComparison.java:242:13:242:15 | uri : URI | provenance | | +| HostComparison.java:239:27:239:53 | getParameter(...) : String | HostComparison.java:239:19:239:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:239:27:239:53 | getParameter(...) : String | HostComparison.java:239:19:239:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:242:13:242:15 | uri : URI | HostComparison.java:242:13:242:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:247:19:247:54 | new URI(...) : URI | HostComparison.java:250:13:250:15 | uri : URI | provenance | | +| HostComparison.java:247:27:247:53 | getParameter(...) : String | HostComparison.java:247:19:247:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:247:27:247:53 | getParameter(...) : String | HostComparison.java:247:19:247:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:250:13:250:15 | uri : URI | HostComparison.java:250:13:250:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:272:19:272:54 | new URI(...) : URI | HostComparison.java:276:13:276:15 | uri : URI | provenance | | +| HostComparison.java:272:27:272:53 | getParameter(...) : String | HostComparison.java:272:19:272:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:272:27:272:53 | getParameter(...) : String | HostComparison.java:272:19:272:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:276:13:276:15 | uri : URI | HostComparison.java:276:13:276:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | +| HostComparison.java:281:19:281:54 | new URI(...) : URI | HostComparison.java:285:13:285:15 | uri : URI | provenance | | +| HostComparison.java:281:27:281:53 | getParameter(...) : String | HostComparison.java:281:19:281:54 | new URI(...) : URI | provenance | Src:MaD:292 Config | +| HostComparison.java:281:27:281:53 | getParameter(...) : String | HostComparison.java:281:19:281:54 | new URI(...) : URI | provenance | Src:MaD:292 MaD:304 | +| HostComparison.java:285:13:285:15 | uri : URI | HostComparison.java:285:13:285:23 | toURL(...) | provenance | MaD:308 Sink:MaD:15 | | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) : String | JakartaWsSSRF.java:15:23:15:25 | url | provenance | Src:MaD:292 Sink:MaD:9 | | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | JavaNetHttpSSRF.java:26:31:26:34 | sink : String | provenance | Src:MaD:292 | | JavaNetHttpSSRF.java:26:23:26:35 | new URI(...) : URI | JavaNetHttpSSRF.java:39:59:39:61 | uri | provenance | Sink:MaD:12 | @@ -1924,18 +1929,22 @@ nodes | HostComparison.java:223:27:223:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | | HostComparison.java:226:13:226:15 | uri : URI | semmle.label | uri : URI | | HostComparison.java:226:13:226:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:231:19:231:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:231:27:231:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:234:13:234:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:234:13:234:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:256:19:256:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:256:27:256:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:260:13:260:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:260:13:260:23 | toURL(...) | semmle.label | toURL(...) | -| HostComparison.java:265:19:265:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | -| HostComparison.java:265:27:265:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | -| HostComparison.java:269:13:269:15 | uri : URI | semmle.label | uri : URI | -| HostComparison.java:269:13:269:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:239:19:239:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:239:27:239:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:242:13:242:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:242:13:242:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:247:19:247:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:247:27:247:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:250:13:250:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:250:13:250:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:272:19:272:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:272:27:272:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:276:13:276:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:276:13:276:23 | toURL(...) | semmle.label | toURL(...) | +| HostComparison.java:281:19:281:54 | new URI(...) : URI | semmle.label | new URI(...) : URI | +| HostComparison.java:281:27:281:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | +| HostComparison.java:285:13:285:15 | uri : URI | semmle.label | uri : URI | +| HostComparison.java:285:13:285:23 | toURL(...) | semmle.label | toURL(...) | | JakartaWsSSRF.java:14:22:14:48 | getParameter(...) : String | semmle.label | getParameter(...) : String | | JakartaWsSSRF.java:15:23:15:25 | url | semmle.label | url | | JavaNetHttpSSRF.java:25:27:25:53 | getParameter(...) : String | semmle.label | getParameter(...) : String | From 8923c35cc728af93f3637d1c05b08faf7d63eddf Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Fri, 16 Oct 2026 10:57:48 +0200 Subject: [PATCH 012/140] Rust: Rename `cfg` to `config` --- rust/extractor/src/main.rs | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/rust/extractor/src/main.rs b/rust/extractor/src/main.rs index 72b6f20c9fb0..55e5934f853c 100644 --- a/rust/extractor/src/main.rs +++ b/rust/extractor/src/main.rs @@ -212,12 +212,12 @@ fn cwd() -> anyhow::Result { } fn main() -> anyhow::Result<()> { - let mut cfg = config::Config::extract().context("failed to load configuration")?; - if cfg.qltest { - qltest::prepare(&mut cfg)?; + let mut config = config::Config::extract().context("failed to load configuration")?; + if config.qltest { + qltest::prepare(&mut config)?; } let start = Instant::now(); - let (flame_layer, _flush_guard) = if let Some(path) = &cfg.logging_flamegraph { + let (flame_layer, _flush_guard) = if let Some(path) = &config.logging_flamegraph { tracing_flame::FlameLayer::with_file(path) .ok() .map(|(a, b)| (Some(a), Some(b))) @@ -229,18 +229,18 @@ fn main() -> anyhow::Result<()> { tracing_subscriber::registry() .with(codeql_extractor::extractor::default_subscriber_with_level( "single_arch", - &cfg.logging_verbosity, + &config.logging_verbosity, )) .with(flame_layer) .init(); - info!("{cfg:#?}\n"); + info!("{config:#?}\n"); - let traps = trap::TrapFileProvider::new(&cfg).context("failed to set up trap files")?; + let traps = trap::TrapFileProvider::new(&config).context("failed to set up trap files")?; let archiver = archive::Archiver { - root: cfg.source_archive_dir.clone(), + root: config.source_archive_dir.clone(), }; let mut extractor = Extractor::new(&archiver, &traps); - let files: Vec = cfg + let files = config .inputs .iter() .map(|file| { @@ -250,7 +250,7 @@ fn main() -> anyhow::Result<()> { // [1]: https://github.com/rust-lang/rust-analyzer/issues/18894#issuecomment-2580014730 dunce::canonicalize(&file).unwrap_or(file) }) - .collect(); + .collect::>(); let manifests = extractor.find_manifests(&files)?; let mut map: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests .iter() @@ -272,13 +272,13 @@ fn main() -> anyhow::Result<()> { } let cwd = cwd()?; log_project_toolchain(); - let (cargo_config, load_cargo_config) = cfg.to_cargo_config(&cwd); - let library_mode = if cfg.extract_dependencies_as_source { + let (cargo_config, load_cargo_config) = config.to_cargo_config(&cwd); + let library_mode = if config.extract_dependencies_as_source { SourceKind::Source } else { SourceKind::Library }; - let source_mode = if cfg.force_library_mode { + let source_mode = if config.force_library_mode { library_mode } else { SourceKind::Source @@ -335,5 +335,5 @@ fn main() -> anyhow::Result<()> { } } - extractor.emit_extraction_diagnostics(start, &cfg) + extractor.emit_extraction_diagnostics(start, &config) } From a5a3b48f2a85d792bc03c4d87123a6884a7dfb54 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Fri, 16 Oct 2026 11:42:11 +0200 Subject: [PATCH 013/140] Rust: Refactor `find_workspace` and add doc comment --- rust/extractor/src/rust_analyzer.rs | 38 ++++++++++++++++------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/rust/extractor/src/rust_analyzer.rs b/rust/extractor/src/rust_analyzer.rs index 25d88ec5ce6b..d3129af12796 100644 --- a/rust/extractor/src/rust_analyzer.rs +++ b/rust/extractor/src/rust_analyzer.rs @@ -217,6 +217,12 @@ fn workspace_members_match(workspace_dir: &AbsPath, members: &[String], target: }) } +/// Finds an enclosing Cargo workspace that includes `manifest`. That is, its +/// `members` patterns include the manifest's directory and its `exclude` +/// patterns do not. +/// +/// Returns `None` if the manifest itself defines a workspace, is not a Cargo +/// manifest, cannot be loaded, or has no matching workspace. fn find_workspace(reader: &mut TomlReader, manifest: &ProjectManifest) -> Option { let ProjectManifest::CargoToml(cargo) = manifest else { return None; @@ -224,12 +230,13 @@ fn find_workspace(reader: &mut TomlReader, manifest: &ProjectManifest) -> Option let parsed_cargo = reader.read(cargo).ok()?; if parsed_cargo.workspace.is_some() { debug!("{cargo} is a workspace"); - return Some(manifest.clone()); + return None; } let Some(parent_dir) = cargo.parent().parent() else { warn!("no parent dir for {cargo}"); return None; }; + // Search upward from `parent_dir` for manifest files. let discovered = ProjectManifest::discover(parent_dir) .map_err(|e| { error!( @@ -240,25 +247,22 @@ fn find_workspace(reader: &mut TomlReader, manifest: &ProjectManifest) -> Option }) .ok()?; discovered - .iter() - .find_map(|it| match it { - ProjectManifest::CargoToml(other) - if cargo.starts_with(other.parent()) - && reader.read(other).is_ok_and(|it| { - it.workspace.as_ref().is_some_and(|w| { - workspace_members_match(other.parent(), &w.members, cargo.parent()) - && !workspace_members_match( - other.parent(), - &w.exclude, - cargo.parent(), - ) - }) - }) => + .into_iter() + .find(|it| { + if let ProjectManifest::CargoToml(other) = it + && cargo.starts_with(other.parent()) + && reader.read(other).is_ok_and(|it| { + it.workspace.as_ref().is_some_and(|w| { + workspace_members_match(other.parent(), &w.members, cargo.parent()) + && !workspace_members_match(other.parent(), &w.exclude, cargo.parent()) + }) + }) { debug!("found workspace {other} containing {cargo}"); - Some(it.clone()) + true + } else { + false } - _ => None, }) .or_else(|| { debug!("no workspace found for {cargo}"); From 87754be930404fc77d52b1ddb8be04043339ebc2 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 09:11:20 +0200 Subject: [PATCH 014/140] Rust: Extract workspace grouping into a helper --- rust/extractor/src/main.rs | 54 ++++++++++++++++++++++++-------------- 1 file changed, 35 insertions(+), 19 deletions(-) diff --git a/rust/extractor/src/main.rs b/rust/extractor/src/main.rs index 55e5934f853c..af650ac5713b 100644 --- a/rust/extractor/src/main.rs +++ b/rust/extractor/src/main.rs @@ -200,6 +200,39 @@ impl<'a> Extractor<'a> { self.steps.push(ExtractionStep::find_manifests(before)); ret } + + fn group_files<'b>( + &mut self, + manifests: &'b [ProjectManifest], + files: &'b [PathBuf], + ) -> HashMap<&'b Path, (&'b ProjectManifest, Vec<&'b Path>)> { + let mut workspaces: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests + .iter() + .map(|manifest| { + ( + manifest.manifest_path().parent().as_ref(), + (manifest, Vec::new()), + ) + }) + .collect(); + + 'outer: for file in files { + for ancestor in file.as_path().ancestors() { + if let Some((_, files)) = workspaces.get_mut(ancestor) { + files.push(file); + continue 'outer; + } + } + self.extract_without_semantics( + file, + SourceKind::Source, + RustAnalyzerNoSemantics::warning("no manifest found"), + ); + } + + workspaces.retain(|_, (_, files)| !files.is_empty()); + workspaces + } } fn cwd() -> anyhow::Result { @@ -252,24 +285,7 @@ fn main() -> anyhow::Result<()> { }) .collect::>(); let manifests = extractor.find_manifests(&files)?; - let mut map: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests - .iter() - .map(|x| (x.manifest_path().parent().as_ref(), (x, Vec::new()))) - .collect(); - - 'outer: for file in &files { - for ancestor in file.as_path().ancestors() { - if let Some((_, files)) = map.get_mut(ancestor) { - files.push(file); - continue 'outer; - } - } - extractor.extract_without_semantics( - file, - SourceKind::Source, - RustAnalyzerNoSemantics::warning("no manifest found"), - ); - } + let workspaces = extractor.group_files(&manifests, &files); let cwd = cwd()?; log_project_toolchain(); let (cargo_config, load_cargo_config) = config.to_cargo_config(&cwd); @@ -285,7 +301,7 @@ fn main() -> anyhow::Result<()> { }; let mut processed_files: HashSet = HashSet::from_iter(files.iter().cloned()); - for (manifest, files) in map.values().filter(|(_, files)| !files.is_empty()) { + for (manifest, files) in workspaces.values() { if let Some((ref db, ref vfs)) = extractor.load_manifest(manifest, &cargo_config, &load_cargo_config) { From f874e86a4bfaed1dce05b7c0e9c21c50103e1b66 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 10:36:46 +0200 Subject: [PATCH 015/140] Rust: Select dependency extraction contexts by feature configuration Defer dependency extraction until all workspaces have been considered, prefer configurations enabling more Cargo features, and use deterministic tie-breaking. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3def7ce6-eea3-4d78-963d-f3675492bb16 --- rust/extractor/src/configuration.rs | 144 ++++++++++++++++++++++++++++ rust/extractor/src/diagnostics.rs | 29 +++++- rust/extractor/src/main.rs | 80 +++++++++------- rust/extractor/src/util.rs | 15 +++ 4 files changed, 232 insertions(+), 36 deletions(-) create mode 100644 rust/extractor/src/configuration.rs create mode 100644 rust/extractor/src/util.rs diff --git a/rust/extractor/src/configuration.rs b/rust/extractor/src/configuration.rs new file mode 100644 index 000000000000..090aff92ef95 --- /dev/null +++ b/rust/extractor/src/configuration.rs @@ -0,0 +1,144 @@ +use crate::util::cmp_option_by; +use itertools::Itertools; +use ra_ap_base_db::SourceDatabase; +use ra_ap_cfg::{CfgAtom, CfgOptions}; +use ra_ap_hir::Semantics; +use ra_ap_ide_db::RootDatabase; +use ra_ap_project_model::ProjectManifest; +use ra_ap_vfs::{FileId, VfsPath}; +use std::cmp::Ordering; +use std::collections::{BTreeMap, HashMap, HashSet, btree_map::Entry}; +use std::path::{Path, PathBuf}; + +/// Returns the enclosing directory of the manifest file. +pub fn manifest_parent(manifest: &ProjectManifest) -> &Path { + manifest.manifest_path().parent().as_ref() +} + +/// Compare two `CfgOptions`. Feature count wins, sorted cfg options break ties. +fn compare_cfg_options(left: &CfgOptions, right: &CfgOptions) -> Ordering { + let feature_count = |cfg_options: &CfgOptions| { + cfg_options + .into_iter() + .filter(|cfg| matches!(cfg, CfgAtom::KeyValue { key, .. } if key.as_str() == "feature")) + .count() + }; + feature_count(left) + .cmp(&feature_count(right)) + .then_with(|| left.into_iter().sorted().cmp(right.into_iter().sorted())) +} + +#[derive(PartialEq, Eq)] +struct DependencySource<'a> { + cfg_options: Option, + manifest: &'a ProjectManifest, +} + +impl Ord for DependencySource<'_> { + fn cmp(&self, other: &Self) -> Ordering { + // Compare by cfg options first, then by manifest parent directory + // (arbitrary but deterministic). + cmp_option_by(&self.cfg_options, &other.cfg_options, compare_cfg_options) + .then_with(|| manifest_parent(self.manifest).cmp(manifest_parent(other.manifest))) + } +} + +impl PartialOrd for DependencySource<'_> { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +/// Map dependency file paths to their highest-priority workspace context. +#[derive(Default)] +pub struct DependencyFiles<'a> { + files: BTreeMap>, +} + +impl<'a> DependencyFiles<'a> { + /// Considers this workspace as an extraction context for a dependency file, + /// retaining only the highest-priority context seen so far. The lexicographically + /// largest workspace path breaks identical-priority ties. + pub fn add_file( + &mut self, + manifest: &'a ProjectManifest, + file_id: FileId, + file: &VfsPath, + semantics: &Semantics<'_, RootDatabase>, + source_files: &HashSet, + ) { + let db = semantics.db; + if let Some(file) = file.as_path().map(<_ as AsRef>::as_ref) + && file.extension().is_some_and(|ext| ext == "rs") + && !source_files.contains(file) + && let root = db.file_source_root(file_id).source_root_id(db) + && db.source_root(root).source_root(db).is_library + { + let cfg_options = semantics + .file_to_module_def(file_id) + .map(|module| module.krate(db).cfg(db).clone()); + self.add_candidate(file, cfg_options, manifest); + } + } + + fn add_candidate( + &mut self, + file: &Path, + cfg_options: Option, + manifest: &'a ProjectManifest, + ) { + let candidate = DependencySource { + cfg_options, + manifest, + }; + match self.files.entry(file.to_owned()) { + Entry::Occupied(mut entry) => { + if candidate > *entry.get() { + entry.insert(candidate); + } + } + Entry::Vacant(entry) => { + entry.insert(candidate); + } + } + } + + /// Groups the selected dependency files by manifest for the dependency extraction pass. + pub fn by_manifests(self) -> HashMap<&'a ProjectManifest, Vec> { + self.files + .into_iter() + .map(|(file, source)| (source.manifest, file)) + .into_group_map() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ra_ap_intern::Symbol; + #[test] + fn selects_highest_priority_in_either_order() { + let cfg = |features: &[&str]| { + let mut cfg = CfgOptions::default(); + for feature in features { + cfg.insert_key_value(Symbol::intern("feature"), Symbol::intern(feature)); + } + cfg + }; + let root = crate::cwd().unwrap(); + let manifest = |path| ProjectManifest::from_manifest_file(root.join(path)).unwrap(); + let candidates = [ + (manifest("z/Cargo.toml"), cfg(&["z"])), + (manifest("a/Cargo.toml"), cfg(&["a", "b"])), + (manifest("b/Cargo.toml"), cfg(&["a", "b"])), + ]; + for order in [[0, 1, 2], [2, 1, 0]] { + let mut plan = DependencyFiles::default(); + for (manifest, cfg) in order.map(|index| &candidates[index]) { + plan.add_candidate(Path::new("lib.rs"), Some(cfg.clone()), manifest); + } + let expected = HashMap::from([(&candidates[2].0, vec![PathBuf::from("lib.rs")])]); + assert_eq!(plan.by_manifests(), expected); + } + } +} diff --git a/rust/extractor/src/diagnostics.rs b/rust/extractor/src/diagnostics.rs index 0db3358d558a..541cef1309e8 100644 --- a/rust/extractor/src/diagnostics.rs +++ b/rust/extractor/src/diagnostics.rs @@ -5,7 +5,7 @@ use chrono::{DateTime, Utc}; use ra_ap_project_model::ProjectManifest; use serde::Serialize; use serde::ser::SerializeMap; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::fmt::Display; use std::fs::File; use std::path::{Path, PathBuf}; @@ -220,13 +220,15 @@ struct ExtractionSummary { type ExtractionDiagnostics = Diagnostics; fn summary(start: Instant, steps: &[ExtractionStep]) -> ExtractionSummary { - let mut number_of_manifests = 0; + // The manifests loaded during extraction. We might load the same manifest + // multiple times, so we use a set to keep track of unique manifests. + let mut manifests = HashSet::new(); let mut number_of_files = 0; let mut durations = HashMap::new(); for step in steps { match &step.action { ExtractionStepKind::LoadManifest => { - number_of_manifests += 1; + manifests.extend(step.file.as_ref()); } ExtractionStepKind::Parse => { number_of_files += 1; @@ -244,7 +246,7 @@ fn summary(start: Instant, steps: &[ExtractionStep]) -> ExtractionSummary { } info!("total duration: {total}"); ExtractionSummary { - number_of_manifests, + number_of_manifests: manifests.len(), number_of_files, durations: DurationsSummary { durations, total }, } @@ -284,3 +286,22 @@ pub fn emit_extraction_diagnostics( .with_context(|| format!("writing to diagnostics file {}", target.display()))?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn counts_unique_manifests() { + let steps: Vec<_> = ["a/Cargo.toml", "b/Cargo.toml", "a/Cargo.toml"] + .into_iter() + .map(|path| ExtractionStep { + action: ExtractionStepKind::LoadManifest, + file: Some(PathBuf::from(path)), + ms: 1, + }) + .collect(); + let summary = summary(Instant::now(), &steps); + assert_eq!(summary.number_of_manifests, 2); + } +} diff --git a/rust/extractor/src/main.rs b/rust/extractor/src/main.rs index af650ac5713b..ef91c5ab508d 100644 --- a/rust/extractor/src/main.rs +++ b/rust/extractor/src/main.rs @@ -5,7 +5,7 @@ use crate::translate::SourceKind; use crate::trap::TrapId; use anyhow::Context; use archive::Archiver; -use ra_ap_base_db::SourceDatabase; +use configuration::{DependencyFiles, manifest_parent}; use ra_ap_hir::Semantics; use ra_ap_ide_db::RootDatabase; use ra_ap_ide_db::line_index::{LineCol, LineIndex}; @@ -14,13 +14,9 @@ use ra_ap_paths::{AbsPathBuf, Utf8PathBuf}; use ra_ap_project_model::{CargoConfig, ProjectManifest}; use ra_ap_vfs::Vfs; use rust_analyzer::{ParseResult, RustAnalyzer}; -use std::collections::HashSet; -use std::hash::RandomState; +use std::collections::{BTreeMap, HashSet}; +use std::path::{Path, PathBuf}; use std::time::Instant; -use std::{ - collections::HashMap, - path::{Path, PathBuf}, -}; use std::{env, fs}; use tracing::{error, info, warn}; use tracing_subscriber::layer::SubscriberExt; @@ -28,6 +24,7 @@ use tracing_subscriber::util::SubscriberInitExt; mod archive; mod config; +mod configuration; mod crate_graph; mod diagnostics; pub mod generated; @@ -36,6 +33,7 @@ mod rust_analyzer; mod toolchain; mod translate; pub mod trap; +mod util; struct Extractor<'a> { archiver: &'a Archiver, @@ -205,15 +203,10 @@ impl<'a> Extractor<'a> { &mut self, manifests: &'b [ProjectManifest], files: &'b [PathBuf], - ) -> HashMap<&'b Path, (&'b ProjectManifest, Vec<&'b Path>)> { - let mut workspaces: HashMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests + ) -> BTreeMap<&'b Path, (&'b ProjectManifest, Vec<&'b Path>)> { + let mut workspaces: BTreeMap<&Path, (&ProjectManifest, Vec<&Path>)> = manifests .iter() - .map(|manifest| { - ( - manifest.manifest_path().parent().as_ref(), - (manifest, Vec::new()), - ) - }) + .map(|manifest| (manifest_parent(manifest), (manifest, Vec::new()))) .collect(); 'outer: for file in files { @@ -299,35 +292,36 @@ fn main() -> anyhow::Result<()> { } else { SourceKind::Source }; - let mut processed_files: HashSet = - HashSet::from_iter(files.iter().cloned()); + let source_files = HashSet::from_iter(files.iter().cloned()); + + let mut dependencies = DependencyFiles::default(); for (manifest, files) in workspaces.values() { - if let Some((ref db, ref vfs)) = + if let Some((db, vfs)) = extractor.load_manifest(manifest, &cargo_config, &load_cargo_config) { let before_crate_graph = Instant::now(); - crate_graph::extract_crate_graph(extractor.traps, db, vfs); + crate_graph::extract_crate_graph(extractor.traps, &db, &vfs); extractor .steps .push(ExtractionStep::crate_graph(before_crate_graph)); - let semantics = Semantics::new(db); + let semantics = Semantics::new(&db); for file in files { - match extractor.load_source(file, &semantics, vfs) { - Ok(()) => extractor.extract_with_semantics(file, &semantics, vfs, source_mode), + match extractor.load_source(file, &semantics, &vfs) { + Ok(()) => extractor.extract_with_semantics(file, &semantics, &vfs, source_mode), Err(e) => extractor.extract_without_semantics(file, source_mode, e), }; } + // The same dependency path may appear in multiple workspaces' VFSs. + // We record it by updating its preferred context, but defer + // extraction until all workspaces have contributed their priority. for (file_id, file) in vfs.iter() { - if let Some(file) = file.as_path().map(<_ as AsRef>::as_ref) - && file.extension().is_some_and(|ext| ext == "rs") - && processed_files.insert(file.to_owned()) - && db - .source_root(db.file_source_root(file_id).source_root_id(db)) - .source_root(db) - .is_library - { - extractor.extract_with_semantics(file, &semantics, vfs, library_mode); - extractor.archiver.archive(file); + dependencies.add_file(manifest, file_id, file, &semantics, &source_files); + } + // A single workspace has no competing configurations, so reuse its loaded database. + if workspaces.len() == 1 { + let selected = std::mem::take(&mut dependencies).by_manifests(); + for file in selected.into_values().flatten() { + extractor.extract_with_semantics(&file, &semantics, &vfs, library_mode); } } } else { @@ -340,6 +334,28 @@ fn main() -> anyhow::Result<()> { } } } + + // For each workspace extract the dependency files for which that workspace + // is the preferred one. + for (manifest, files) in dependencies.by_manifests() { + if let Some((db, vfs)) = + extractor.load_manifest(manifest, &cargo_config, &load_cargo_config) + { + let semantics = Semantics::new(&db); + for file in files { + extractor.extract_with_semantics(&file, &semantics, &vfs, library_mode); + } + } else { + for file in files { + extractor.extract_without_semantics( + &file, + library_mode, + RustAnalyzerNoSemantics::warning("unable to reload manifest"), + ); + } + } + } + let builtins_dir = env::var("CODEQL_EXTRACTOR_RUST_ROOT") .map(|path| Path::new(&path).join("tools").join("builtins"))?; let builtins = fs::read_dir(builtins_dir).context("failed to read builtins directory")?; diff --git a/rust/extractor/src/util.rs b/rust/extractor/src/util.rs new file mode 100644 index 000000000000..a12ca6de74a1 --- /dev/null +++ b/rust/extractor/src/util.rs @@ -0,0 +1,15 @@ +use std::cmp::Ordering; + +/// Compares options using a custom comparator, with `None` ordered before `Some`. +pub(crate) fn cmp_option_by( + left: &Option, + right: &Option, + compare: impl FnOnce(&T, &T) -> Ordering, +) -> Ordering { + match (left, right) { + (Some(left), Some(right)) => compare(left, right), + (Some(_), None) => Ordering::Greater, + (None, Some(_)) => Ordering::Less, + (None, None) => Ordering::Equal, + } +} From 34886ac027b2a6a78317512ec6745dc3a6c7c9d8 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 10:39:12 +0200 Subject: [PATCH 016/140] Rust: Test deterministic dependency configuration selection Add tests for extraction of dependency files with different features enabled in different manifest files. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3def7ce6-eea3-4d78-963d-f3675492bb16 --- rust/extractor/src/diagnostics.rs | 2 +- .../dependency-features/.cargo/config.toml | 7 ++ .../dependencies/alpha/.cargo-checksum.json | 1 + .../dependencies/alpha/Cargo.toml | 8 ++ .../dependencies/alpha/src/lib.rs | 21 +++++ .../dependencies/beta/.cargo-checksum.json | 1 + .../dependencies/beta/Cargo.toml | 8 ++ .../dependencies/beta/src/lib.rs | 21 +++++ .../dependency-features/functions.ql | 13 ++++ .../functions.rich.expected | 4 + .../functions.rich.reversed.expected | 4 + .../functions.tie.expected | 4 + .../functions.tie.reversed.expected | 4 + .../source/first/src/lib.rs | 4 + .../source/second/src/lib.rs | 4 + .../test_dependency_features.py | 78 +++++++++++++++++++ 16 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 rust/ql/integration-tests/dependency-features/.cargo/config.toml create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml create mode 100644 rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/functions.ql create mode 100644 rust/ql/integration-tests/dependency-features/functions.rich.expected create mode 100644 rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected create mode 100644 rust/ql/integration-tests/dependency-features/functions.tie.expected create mode 100644 rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected create mode 100644 rust/ql/integration-tests/dependency-features/source/first/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/source/second/src/lib.rs create mode 100644 rust/ql/integration-tests/dependency-features/test_dependency_features.py diff --git a/rust/extractor/src/diagnostics.rs b/rust/extractor/src/diagnostics.rs index 541cef1309e8..ab88192c5d3e 100644 --- a/rust/extractor/src/diagnostics.rs +++ b/rust/extractor/src/diagnostics.rs @@ -292,7 +292,7 @@ mod tests { use super::*; #[test] - fn counts_unique_manifests() { + fn counts_unique_manifests_but_includes_reload_duration() { let steps: Vec<_> = ["a/Cargo.toml", "b/Cargo.toml", "a/Cargo.toml"] .into_iter() .map(|path| ExtractionStep { diff --git a/rust/ql/integration-tests/dependency-features/.cargo/config.toml b/rust/ql/integration-tests/dependency-features/.cargo/config.toml new file mode 100644 index 000000000000..6f5d1edd7231 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/.cargo/config.toml @@ -0,0 +1,7 @@ +# Add a source definition backed by the directories in "dependencies" +[source.test-dependencies] +directory = "dependencies" + +# Map crates.io to our local source definition +[source.crates-io] +replace-with = "test-dependencies" diff --git a/rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json b/rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json new file mode 100644 index 000000000000..09bf61c6fa41 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/alpha/.cargo-checksum.json @@ -0,0 +1 @@ +{"files": {}, "package": null} diff --git a/rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml b/rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml new file mode 100644 index 000000000000..151c6e5949f1 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/alpha/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "alpha" +version = "0.1.0" +edition = "2024" + +[features] +a = [] +b = [] diff --git a/rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs b/rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs new file mode 100644 index 000000000000..c2aafb3b633e --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/alpha/src/lib.rs @@ -0,0 +1,21 @@ +#[cfg(feature = "a")] +pub fn a() {} + +#[cfg(feature = "b")] +pub fn b() {} + +#[cfg(feature = "b")] +#[macro_export] +macro_rules! invoke { + () => { + $crate::b() + }; +} + +#[cfg(not(feature = "b"))] +#[macro_export] +macro_rules! invoke { + () => { + $crate::a() + }; +} diff --git a/rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json b/rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json new file mode 100644 index 000000000000..09bf61c6fa41 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/beta/.cargo-checksum.json @@ -0,0 +1 @@ +{"files": {}, "package": null} diff --git a/rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml b/rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml new file mode 100644 index 000000000000..b9c6e4f70445 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/beta/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "beta" +version = "0.1.0" +edition = "2024" + +[features] +a = [] +b = [] diff --git a/rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs b/rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs new file mode 100644 index 000000000000..c2aafb3b633e --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/dependencies/beta/src/lib.rs @@ -0,0 +1,21 @@ +#[cfg(feature = "a")] +pub fn a() {} + +#[cfg(feature = "b")] +pub fn b() {} + +#[cfg(feature = "b")] +#[macro_export] +macro_rules! invoke { + () => { + $crate::b() + }; +} + +#[cfg(not(feature = "b"))] +#[macro_export] +macro_rules! invoke { + () => { + $crate::a() + }; +} diff --git a/rust/ql/integration-tests/dependency-features/functions.ql b/rust/ql/integration-tests/dependency-features/functions.ql new file mode 100644 index 000000000000..a475c9258072 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.ql @@ -0,0 +1,13 @@ +import rust + +from Call c, string target +where + c.getFile().getRelativePath() in [ + "first/src/lib.rs", "second/src/lib.rs", "src/lib.rs", "build.rs" + ] and + ( + target = c.getStaticTarget().getCanonicalPath() + or + not exists(c.getStaticTarget()) and target = "" + ) +select c, target diff --git a/rust/ql/integration-tests/dependency-features/functions.rich.expected b/rust/ql/integration-tests/dependency-features/functions.rich.expected new file mode 100644 index 000000000000..796c4e1aae87 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.rich.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/first/src/lib.rs:3:5:3:17 | ...::a(...) | beta::a | +| source/second/src/lib.rs:2:5:2:18 | ...::a(...) | alpha::a | +| source/second/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | diff --git a/rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected b/rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected new file mode 100644 index 000000000000..f352f77ba179 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.rich.reversed.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::a(...) | alpha::a | +| source/first/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | +| source/second/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/second/src/lib.rs:3:5:3:17 | ...::a(...) | beta::a | diff --git a/rust/ql/integration-tests/dependency-features/functions.tie.expected b/rust/ql/integration-tests/dependency-features/functions.tie.expected new file mode 100644 index 000000000000..31cbfba529fa --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.tie.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::a(...) | | +| source/first/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | +| source/second/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/second/src/lib.rs:3:5:3:17 | ...::a(...) | | diff --git a/rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected b/rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected new file mode 100644 index 000000000000..a4475eb87c83 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/functions.tie.reversed.expected @@ -0,0 +1,4 @@ +| source/first/src/lib.rs:2:5:2:18 | ...::b(...) | alpha::b | +| source/first/src/lib.rs:3:5:3:17 | ...::a(...) | | +| source/second/src/lib.rs:2:5:2:18 | ...::a(...) | | +| source/second/src/lib.rs:3:5:3:17 | ...::b(...) | beta::b | diff --git a/rust/ql/integration-tests/dependency-features/source/first/src/lib.rs b/rust/ql/integration-tests/dependency-features/source/first/src/lib.rs new file mode 100644 index 000000000000..a4a49f20b4ce --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/source/first/src/lib.rs @@ -0,0 +1,4 @@ +pub fn emit() { + alpha::invoke!(); + beta::invoke!(); +} diff --git a/rust/ql/integration-tests/dependency-features/source/second/src/lib.rs b/rust/ql/integration-tests/dependency-features/source/second/src/lib.rs new file mode 100644 index 000000000000..a4a49f20b4ce --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/source/second/src/lib.rs @@ -0,0 +1,4 @@ +pub fn emit() { + alpha::invoke!(); + beta::invoke!(); +} diff --git a/rust/ql/integration-tests/dependency-features/test_dependency_features.py b/rust/ql/integration-tests/dependency-features/test_dependency_features.py new file mode 100644 index 000000000000..074109132d20 --- /dev/null +++ b/rust/ql/integration-tests/dependency-features/test_dependency_features.py @@ -0,0 +1,78 @@ +import json +from textwrap import dedent + +import commands +import pytest + + +@pytest.mark.parametrize( + ("configurations", "reverse"), + [ + pytest.param( + configurations, + reverse, + marks=pytest.mark.ql_test( + expected=f".{expected}{'.reversed' if reverse else ''}.expected" + ), + id=f"{name}{'-reversed' if reverse else ''}", + ) + for name, expected, configurations in [ + ("more-features", "rich", [(["a", "b"], ["a"]), (["a"], ["a", "b"])]), + ("equal-features", "tie", [(["a"], ["b"]), (["b"], ["a"])]), + ] + for reverse in [False, True] + ], +) +def test_dependency_features(codeql, rust, cwd, configurations, reverse): + """ + Two independent workspaces, `source/first` and `source/second`, share the + local dependencies `alpha` and `beta`, but enable different Cargo features + (their `Cargo.toml` are created in this test). + + Below is an overview of the files in the test setup. + ``` + dependency-features/ + |-- dependencies/ + | |-- alpha/ + | | |-- Cargo.toml + | | `-- src/lib.rs # feature-gated functions and invoke! macro + | `-- beta/ + | |-- Cargo.toml + | `-- src/lib.rs # feature-gated functions and invoke! macro + |-- source/ + | |-- first/ + | | |-- Cargo.toml # generated by this test for each configuration + | | `-- src/lib.rs # calls alpha::invoke! and beta::invoke! + | `-- second/ + | |-- Cargo.toml # generated by this test for each configuration + | `-- src/lib.rs # calls alpha::invoke! and beta::invoke! + ``` + + Each `src/lib.rs` file calls a macro from each dependency. The macros expand + to different calls depending on the enabled features. The `functions.ql` + query checks which calls and targets are extracted for the `src/lib.rs` + files. + """ + if reverse: + configurations = configurations[::-1] + for workspace, features in zip(["first", "second"], configurations): + manifest = cwd / "source" / workspace / "Cargo.toml" + manifest.write_text( + dedent(f"""\ + [package] + name = "{workspace}" + version = "0.1.0" + edition = "2024" + + [workspace] + + [dependencies] + """) + + "\n".join( + f'{name} = {{ version = "=0.1.0", features = {json.dumps(enabled)} }}' + for name, enabled in zip(["alpha", "beta"], features) + ) + + "\n" + ) + commands.run(f'cargo check --quiet --manifest-path "{manifest}"') + codeql.database.create(source_root="source") From c3fdbd89a03e635b70c5225bc43ceac4a3c42718 Mon Sep 17 00:00:00 2001 From: Simon Friis Vindum Date: Tue, 20 Oct 2026 11:29:31 +0200 Subject: [PATCH 017/140] Rust: Update extraction-step expectations for dependency reloads Regenerate existing integration-test expectations to account for manifest reloads in the deferred dependency extraction pass. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 3def7ce6-eea3-4d78-963d-f3675492bb16 --- rust/ql/integration-tests/workspace-with-glob/steps.expected | 1 + 1 file changed, 1 insertion(+) diff --git a/rust/ql/integration-tests/workspace-with-glob/steps.expected b/rust/ql/integration-tests/workspace-with-glob/steps.expected index 4b0e6ed828b6..3ea9ee61bed3 100644 --- a/rust/ql/integration-tests/workspace-with-glob/steps.expected +++ b/rust/ql/integration-tests/workspace-with-glob/steps.expected @@ -6,6 +6,7 @@ | lib/src/lib.rs:0:0:0:0 | Extract(lib/src/lib.rs) | | lib/src/lib.rs:0:0:0:0 | Parse(lib/src/lib.rs) | | other/Cargo.toml:0:0:0:0 | LoadManifest(other/Cargo.toml) | +| other/Cargo.toml:0:0:0:0 | LoadManifest(other/Cargo.toml) | | other/src/lib.rs:0:0:0:0 | Extract(other/src/lib.rs) | | other/src/lib.rs:0:0:0:0 | LoadSource(other/src/lib.rs) | | other/src/lib.rs:0:0:0:0 | Parse(other/src/lib.rs) | From 5dcdf16a9626e63959bde982ea8d24af301769a5 Mon Sep 17 00:00:00 2001 From: Owen Mansel-Chan Date: Thu, 22 Oct 2026 15:28:24 +0100 Subject: [PATCH 018/140] Fix multiline regular expression sanitizer guards Account for multiline and unknown flags in the shared whole-string anchoring check so generic and membership-based taint sanitizers agree. Add inline regression coverage for the reported command-injection and reflected-XSS cases, regex API variants, and class-validator decorators. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../2026-10-22-multiline-regexp-sanitizer.md | 4 + .../ql/lib/semmle/javascript/Regexp.qll | 28 +++-- .../RegExpSanitizer/decorators.ts | 15 +++ .../library-tests/RegExpSanitizer/guards.js | 112 ++++++++++++++++++ .../library-tests/RegExpSanitizer/routes.js | 36 ++++++ .../RegExpSanitizer/test.expected | 0 .../library-tests/RegExpSanitizer/test.ql | 40 +++++++ 7 files changed, 227 insertions(+), 8 deletions(-) create mode 100644 javascript/ql/lib/change-notes/2026-10-22-multiline-regexp-sanitizer.md create mode 100644 javascript/ql/test/library-tests/RegExpSanitizer/decorators.ts create mode 100644 javascript/ql/test/library-tests/RegExpSanitizer/guards.js create mode 100644 javascript/ql/test/library-tests/RegExpSanitizer/routes.js create mode 100644 javascript/ql/test/library-tests/RegExpSanitizer/test.expected create mode 100644 javascript/ql/test/library-tests/RegExpSanitizer/test.ql diff --git a/javascript/ql/lib/change-notes/2026-10-22-multiline-regexp-sanitizer.md b/javascript/ql/lib/change-notes/2026-10-22-multiline-regexp-sanitizer.md new file mode 100644 index 000000000000..d77bd99c1b0e --- /dev/null +++ b/javascript/ql/lib/change-notes/2026-10-22-multiline-regexp-sanitizer.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* Regular expression checks using multiline (`m`) or unknown flags are no longer treated as generic taint sanitizers just because the pattern is anchored with `^` and `$`. This can reveal previously missed alerts, including command injection and reflected cross-site scripting. diff --git a/javascript/ql/lib/semmle/javascript/Regexp.qll b/javascript/ql/lib/semmle/javascript/Regexp.qll index c42df3939c22..49702214bc62 100644 --- a/javascript/ql/lib/semmle/javascript/Regexp.qll +++ b/javascript/ql/lib/semmle/javascript/Regexp.qll @@ -1275,16 +1275,26 @@ module RegExp { bindingset[flags] predicate maybeDotAll(string flags) { flags = unknownFlag() or isDotAll(flags) } - /** Holds if `term` and all of its disjuncts are anchored on both ends. */ + /** + * Holds if `term` and all of its disjuncts are anchored to both ends of the input. + * + * Does not hold if a regular expression using the term has multiline or unknown flags. + */ predicate isFullyAnchoredTerm(RegExpTerm term) { - exists(RegExpSequence seq | term = seq | - seq.getChild(0) instanceof RegExpCaret and - seq.getLastChild() instanceof RegExpDollar + not exists(DataFlow::RegExpCreationNode regexp | + regexp.getRoot() = term.getRootTerm() and + maybeMultiline(regexp.tryGetFlags()) + ) and + ( + exists(RegExpSequence seq | term = seq | + seq.getChild(0) instanceof RegExpCaret and + seq.getLastChild() instanceof RegExpDollar + ) + or + isFullyAnchoredTerm(term.(RegExpGroup).getAChild()) + or + isFullyAnchoredAlt(term, term.getNumChild()) ) - or - isFullyAnchoredTerm(term.(RegExpGroup).getAChild()) - or - isFullyAnchoredAlt(term, term.getNumChild()) } /** Holds if the first `i` disjuncts of `term` are fully anchored. */ @@ -1330,6 +1340,8 @@ module RegExp { * * Specifically, whitelisting regexps such as `^(foo|bar)$` sanitize matches in the true case. * Inverted character classes such as `[^a-z]` or `\W` sanitize matches in the false case. + * Anchored terms do not sanitize matches if a regular expression using the term has + * multiline or unknown flags. */ predicate isGenericRegExpSanitizer(RegExpTerm term, boolean outcome) { term.isRootTerm() and diff --git a/javascript/ql/test/library-tests/RegExpSanitizer/decorators.ts b/javascript/ql/test/library-tests/RegExpSanitizer/decorators.ts new file mode 100644 index 000000000000..91a83e23d240 --- /dev/null +++ b/javascript/ql/test/library-tests/RegExpSanitizer/decorators.ts @@ -0,0 +1,15 @@ +import { Matches } from "class-validator"; + +class Validated { + @Matches(/^\w+$/) // $ sanitizer + singleLine: string; + + @Matches(/^\w+$/m) + multiline: string; + + @Matches(new RegExp("^\\w+$")) // $ sanitizer + constructedSingleLine: string; + + @Matches(new RegExp("^\\w+$", "m")) + constructedMultiline: string; +} diff --git a/javascript/ql/test/library-tests/RegExpSanitizer/guards.js b/javascript/ql/test/library-tests/RegExpSanitizer/guards.js new file mode 100644 index 000000000000..359329938585 --- /dev/null +++ b/javascript/ql/test/library-tests/RegExpSanitizer/guards.js @@ -0,0 +1,112 @@ +function literals() { + const value = source(); + sink(value); // $ flow + + if (/^[\w.-]+$/m.test(value)) { + sink(value); // $ flow + } + if (/^[\w.-]+$/.test(value)) { + sink(value); + } else { + sink(value); // $ flow + } + if (/^[\w.-]+$/i.test(value)) { + sink(value); + } + if (/^[\w.-]+$/s.test(value)) { + sink(value); + } + if (/^[\w.-]+$/m.exec(value)) { + sink(value); // $ flow + } + if (/^[\w.-]+$/.exec(value)) { + sink(value); + } + if (value.match(/^[\w.-]+$/m)) { + sink(value); // $ flow + } + if (value.match(/^[\w.-]+$/)) { + sink(value); + } + if (value.match("^[\\w.-]+$")) { + sink(value); + } + if (value.matchAll(/^[\w.-]+$/gm)) { + sink(value); // $ flow + } +} + +function references() { + const value = source(); + const regexp = /^[\w.-]+$/m; + if (regexp.test(value)) { + sink(value); // $ flow + } + let match; + if (match = regexp.exec(value)) { + sink(value); // $ flow + } +} + +function constructors(flags) { + const value = source(); + if (new RegExp("^[\\w.-]+$", "m").test(value)) { + sink(value); // $ flow + } + if (RegExp("^[\\w.-]+$", "im").test(value)) { + sink(value); // $ flow + } + const multiline = "m"; + if (new RegExp("^[\\w.-]+$", multiline).test(value)) { + sink(value); // $ flow + } + if (new RegExp("^[\\w.-]+$", flags).test(value)) { + sink(value); // $ flow + } + if (new RegExp("^[\\w.-]+$").test(value)) { + sink(value); + } + if (new RegExp("^[\\w.-]+$", "").test(value)) { + sink(value); + } + if (new RegExp("^[\\w.-]+$", "i").test(value)) { + sink(value); + } + if (new RegExp(/^[\w.-]+$/m).test(value)) { + sink(value); // $ flow + } + if (new RegExp(/^[\w.-]+$/, "m").test(value)) { + sink(value); // $ flow + } +} + +function groupsAndAlternatives() { + const value = source(); + if (/(^[\w.-]+$)/m.test(value)) { + sink(value); // $ flow + } + if (/(^[\w.-]+$)/.test(value)) { + sink(value); + } + if (/^foo$|^bar$/m.test(value)) { + sink(value); // $ flow + } + if (/^foo$|^bar$/.test(value)) { + sink(value); + } +} + +function negativeChecks(flags) { + const value = source(); + if (/[^a-z]/m.test(value)) { + sink(value); // $ flow + } else { + sink(value); + } + if (!/\W/m.test(value)) { + sink(value); + } + if (!new RegExp("[^a-z]", flags).test(value)) { + sink(value); + } +} diff --git a/javascript/ql/test/library-tests/RegExpSanitizer/routes.js b/javascript/ql/test/library-tests/RegExpSanitizer/routes.js new file mode 100644 index 000000000000..dabde8e13636 --- /dev/null +++ b/javascript/ql/test/library-tests/RegExpSanitizer/routes.js @@ -0,0 +1,36 @@ +const cp = require("child_process"); +const app = require("express")(); + +app.get("/a", (req, res) => { + const dir = req.query.dir; + if (/^[\w.-]+$/m.test(dir)) { + cp.exec("ls " + dir); // $ command-injection + } + res.send("ok"); +}); + +app.get("/b", (req, res) => { + const dir = req.query.dir; + if (/^[\w.-]+$/.test(dir)) { + cp.exec("ls " + dir); + } + res.send("ok"); +}); + +app.get("/c", (req, res) => { + cp.exec("ls " + req.query.dir); // $ command-injection + res.send("ok"); +}); + +app.get("/d", (req, res) => { + const name = req.query.name; + if (/^\w+$/m.test(name)) { + res.send("

    Hello " + name + "

    "); // $ reflected-xss + } else { + res.send("bad name"); + } +}); + +app.get("/e", (req, res) => { + res.send("

    Hello " + req.query.name + "

    "); // $ reflected-xss +}); diff --git a/javascript/ql/test/library-tests/RegExpSanitizer/test.expected b/javascript/ql/test/library-tests/RegExpSanitizer/test.expected new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/javascript/ql/test/library-tests/RegExpSanitizer/test.ql b/javascript/ql/test/library-tests/RegExpSanitizer/test.ql new file mode 100644 index 000000000000..107b616d81bd --- /dev/null +++ b/javascript/ql/test/library-tests/RegExpSanitizer/test.ql @@ -0,0 +1,40 @@ +import javascript +import utils.test.InlineExpectationsTest +import semmle.javascript.frameworks.ClassValidator +import semmle.javascript.security.dataflow.CommandInjectionQuery as CommandInjection +import semmle.javascript.security.dataflow.ReflectedXssQuery as ReflectedXss + +module TestConfig implements DataFlow::ConfigSig { + predicate isSource(DataFlow::Node source) { + source.(DataFlow::CallNode).getCalleeName() = "source" + } + + predicate isSink(DataFlow::Node sink) { + sink = any(DataFlow::CallNode call | call.getCalleeName() = "sink").getArgument(0) + } +} + +module TestFlow = TaintTracking::Global; + +module InlineTest implements TestSig { + string getARelevantTag() { result = ["flow", "command-injection", "reflected-xss", "sanitizer"] } + + predicate hasActualResult(Location location, string element, string tag, string value) { + element = "" and + value = "" and + exists(DataFlow::Node node | + location = node.getLocation() and + ( + tag = "flow" and TestFlow::flowTo(node) + or + tag = "command-injection" and CommandInjection::CommandInjectionFlow::flowTo(node) + or + tag = "reflected-xss" and ReflectedXss::ReflectedXssFlow::flowTo(node) + or + tag = "sanitizer" and node = ClassValidator::sanitizingDecorator() + ) + ) + } +} + +import MakeTest From e6b9042608c15df1fae9cbfb40a7f6e79b93c04a Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Mon, 12 Oct 2026 14:09:48 +0100 Subject: [PATCH 019/140] C++: Fix const'ness in the map tests. The containers were incorrectly using 'char*' instead of 'const char*' which meant it would not type check when attempting to use string literals as keys (if we had been using a real implementation of those containers). --- .../dataflow/taint-tests/map.cpp | 132 +++++++++--------- 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/cpp/ql/test/library-tests/dataflow/taint-tests/map.cpp b/cpp/ql/test/library-tests/dataflow/taint-tests/map.cpp index 0c6bcedc88bc..da72af69f559 100644 --- a/cpp/ql/test/library-tests/dataflow/taint-tests/map.cpp +++ b/cpp/ql/test/library-tests/dataflow/taint-tests/map.cpp @@ -8,17 +8,17 @@ char *source(); void sink(char *); void sink(const char *); void sink(bool); -void sink(std::pair); -void sink(std::map); -void sink(std::map::iterator); -void sink(std::unordered_map); -void sink(std::unordered_map::iterator); -void sink(std::unordered_map >); -void sink(std::unordered_map >::iterator); +void sink(std::pair); +void sink(std::map); +void sink(std::map::iterator); +void sink(std::unordered_map); +void sink(std::unordered_map::iterator); +void sink(std::unordered_map >); +void sink(std::unordered_map >::iterator); void test_pair() { - std::pair a, b, c; + std::pair a, b, c; a.first = "123"; sink(a.first); @@ -35,36 +35,36 @@ void test_pair() sink(c.second); // $ ast,ir sink(c); // $ ir MISSING: ast - std::pair d("123", "456"); + std::pair d("123", "456"); sink(d.first); sink(d.second); sink(d); - std::pair e(source(), "456"); + std::pair e(source(), "456"); sink(e.first); // $ ast,ir sink(e.second); sink(e); // $ ir MISSING: ast - std::pair f("123", source()); + std::pair f("123", source()); sink(f.first); sink(f.second); // $ ast,ir sink(f); // $ ast,ir - std::pair g(f); + std::pair g(f); sink(g.first); sink(g.second); // $ ast,ir sink(g); // $ ast,ir - std::pair h; + std::pair h; h = f; sink(h.first); sink(h.second); // $ ast,ir sink(h); // $ ast,ir - std::pair i("123", "456"); - std::pair j("123", source()); - std::pair k("123", source()); - std::pair l("123", "456"); + std::pair i("123", "456"); + std::pair j("123", source()); + std::pair k("123", source()); + std::pair l("123", "456"); i.swap(j); k.swap(l); sink(i.first); @@ -90,7 +90,7 @@ void test_pair() sink(make_pair("123", source()).first); sink(make_pair("123", source()).second); // $ ast,ir - std::pair, char *> m; + std::pair, const char *> m; m = make_pair(make_pair("123", source()), "789"); sink(m); // $ MISSING: ast,ir sink(m.first); // $ MISSING: ast,ir @@ -102,12 +102,12 @@ void test_pair() void test_map() { // insert - std::map m1, m2, m3, m4, m5, m6; + std::map m1, m2, m3, m4, m5, m6; sink(m1.insert(std::make_pair("abc", "def")).first); sink(m2.insert(std::make_pair("abc", source())).first); sink(m3.insert(std::make_pair(source(), "def")).first); // $ MISSING: ast,ir - sink(m4.insert(m4.begin(), std::pair("abc", source()))); // $ ast,ir + sink(m4.insert(m4.begin(), std::pair("abc", source()))); // $ ast,ir sink(m5.insert_or_assign("abc", source()).first); sink(m6.insert_or_assign(m6.begin(), "abc", source())); // $ ast,ir sink(m1); @@ -130,9 +130,9 @@ void test_map() sink(m6.find("def")); // $ SPURIOUS: ast,ir // copy constructors and assignment - std::map m7(m2); - std::map m8 = m2; - std::map m9; + std::map m7(m2); + std::map m8 = m2; + std::map m9; m9 = m2; sink(m7); // $ ast,ir sink(m8); // $ ast,ir @@ -142,7 +142,7 @@ void test_map() sink(m9.find("abc")); // $ ast,ir // iterators - std::map::iterator i1, i2, i3; + std::map::iterator i1, i2, i3; for (i1 = m1.begin(); i1 != m1.end(); i1++) { sink(*i1); @@ -163,7 +163,7 @@ void test_map() } // array-like access - std::map m10, m11, m12, m13; + std::map m10, m11, m12, m13; sink(m10["abc"] = "def"); sink(m11["abc"] = source()); // $ ast,ir sink(m12.at("abc") = "def"); @@ -174,7 +174,7 @@ void test_map() sink(m13["abc"]); // $ ast,ir // ranges - std::map m14; + std::map m14; m14.insert(std::make_pair("a", "a")); m14.insert(std::make_pair("b", source())); m14.insert(std::make_pair("c", source())); @@ -187,9 +187,9 @@ void test_map() sink(m14.equal_range("c").second); // swap - std::map m15, m16, m17, m18; - m15.insert(std::pair(source(), source())); - m18.insert(std::pair(source(), source())); + std::map m15, m16, m17, m18; + m15.insert(std::pair(source(), source())); + m18.insert(std::pair(source(), source())); sink(m15); // $ ast,ir sink(m16); sink(m17); @@ -202,11 +202,11 @@ void test_map() sink(m18); // $ SPURIOUS: ast // merge - std::map m19, m20, m21, m22; - m19.insert(std::pair(source(), source())); - m20.insert(std::pair("abc", "def")); - m21.insert(std::pair("abc", "def")); - m22.insert(std::pair(source(), source())); + std::map m19, m20, m21, m22; + m19.insert(std::pair(source(), source())); + m20.insert(std::pair("abc", "def")); + m21.insert(std::pair("abc", "def")); + m22.insert(std::pair(source(), source())); sink(m19); // $ ast,ir sink(m20); sink(m21); @@ -219,9 +219,9 @@ void test_map() sink(m22); // $ ast,ir // erase, clear - std::map m23; - m23.insert(std::pair(source(), source())); - m23.insert(std::pair(source(), source())); + std::map m23; + m23.insert(std::pair(source(), source())); + m23.insert(std::pair(source(), source())); sink(m23); // $ ast,ir=223:49 ast,ir=224:49 sink(m23.erase(m23.begin())); // $ ast,ir=223:49 ast,ir=224:49 sink(m23); // $ ast,ir=223:49 ast,ir=224:49 @@ -229,7 +229,7 @@ void test_map() sink(m23); // $ SPURIOUS: ast,ir=223:49 ast,ir=224:49 // emplace, emplace_hint - std::map m24, m25; + std::map m24, m25; sink(m24.emplace("abc", "def").first); sink(m24); sink(m24.emplace("abc", source()).first); @@ -240,7 +240,7 @@ void test_map() sink(m25); // $ ast,ir // try_emplace - std::map m26, m27; + std::map m26, m27; sink(m26.try_emplace("abc", "def").first); sink(m26); sink(m26.try_emplace("abc", source()).first); @@ -254,12 +254,12 @@ void test_map() void test_unordered_map() { // insert - std::unordered_map m1, m2, m3, m4, m5, m6; + std::unordered_map m1, m2, m3, m4, m5, m6; sink(m1.insert(std::make_pair("abc", "def")).first); sink(m2.insert(std::make_pair("abc", source())).first); sink(m3.insert(std::make_pair(source(), "def")).first); // $ MISSING: ast,ir - sink(m4.insert(m4.begin(), std::pair("abc", source()))); // $ ast,ir + sink(m4.insert(m4.begin(), std::pair("abc", source()))); // $ ast,ir sink(m5.insert_or_assign("abc", source()).first); sink(m6.insert_or_assign(m6.begin(), "abc", source())); // $ ast,ir sink(m1); @@ -282,9 +282,9 @@ void test_unordered_map() sink(m6.find("def")); // $ SPURIOUS: ast,ir // copy constructors and assignment - std::unordered_map m7(m2); - std::unordered_map m8 = m2; - std::unordered_map m9; + std::unordered_map m7(m2); + std::unordered_map m8 = m2; + std::unordered_map m9; m9 = m2; sink(m7); // $ ast,ir sink(m8); // $ ast,ir @@ -294,7 +294,7 @@ void test_unordered_map() sink(m9.find("abc")); // $ ast,ir // iterators - std::unordered_map::iterator i1, i2, i3; + std::unordered_map::iterator i1, i2, i3; for (i1 = m1.begin(); i1 != m1.end(); i1++) { sink(*i1); @@ -315,7 +315,7 @@ void test_unordered_map() } // array-like access - std::unordered_map m10, m11, m12, m13; + std::unordered_map m10, m11, m12, m13; sink(m10["abc"] = "def"); sink(m11["abc"] = source()); // $ ast,ir sink(m12.at("abc") = "def"); @@ -326,7 +326,7 @@ void test_unordered_map() sink(m13["abc"]); // $ ast,ir // ranges - std::unordered_map m14; + std::unordered_map m14; m14.insert(std::make_pair("a", "a")); m14.insert(std::make_pair("b", source())); m14.insert(std::make_pair("c", source())); @@ -336,9 +336,9 @@ void test_unordered_map() sink(m14.equal_range("c").second); // swap - std::unordered_map m15, m16, m17, m18; - m15.insert(std::pair(source(), source())); - m18.insert(std::pair(source(), source())); + std::unordered_map m15, m16, m17, m18; + m15.insert(std::pair(source(), source())); + m18.insert(std::pair(source(), source())); sink(m15); // $ ast,ir sink(m16); sink(m17); @@ -351,11 +351,11 @@ void test_unordered_map() sink(m18); // $ SPURIOUS: ast // merge - std::unordered_map m19, m20, m21, m22; - m19.insert(std::pair(source(), source())); - m20.insert(std::pair("abc", "def")); - m21.insert(std::pair("abc", "def")); - m22.insert(std::pair(source(), source())); + std::unordered_map m19, m20, m21, m22; + m19.insert(std::pair(source(), source())); + m20.insert(std::pair("abc", "def")); + m21.insert(std::pair("abc", "def")); + m22.insert(std::pair(source(), source())); sink(m19); // $ ast,ir sink(m20); sink(m21); @@ -368,9 +368,9 @@ void test_unordered_map() sink(m22); // $ ast,ir // erase, clear - std::unordered_map m23; - m23.insert(std::pair(source(), source())); - m23.insert(std::pair(source(), source())); + std::unordered_map m23; + m23.insert(std::pair(source(), source())); + m23.insert(std::pair(source(), source())); sink(m23); // $ ast,ir=372:49 ast,ir=373:49 sink(m23.erase(m23.begin())); // $ ast,ir=372:49 ast,ir=373:49 sink(m23); // $ ast,ir=372:49 ast,ir=373:49 @@ -378,7 +378,7 @@ void test_unordered_map() sink(m23); // $ SPURIOUS: ast,ir=372:49 ast,ir=373:49 // emplace, emplace_hint - std::unordered_map m24, m25; + std::unordered_map m24, m25; sink(m24.emplace("abc", "def").first); sink(m24); sink(m24.emplace("abc", source()).first); @@ -389,7 +389,7 @@ void test_unordered_map() sink(m25); // $ ast,ir // try_emplace - std::unordered_map m26, m27, m28; + std::unordered_map m26, m27, m28; sink(m26.try_emplace("abc", "def").first); sink(m26.try_emplace("abc", "def").second); sink(m26); @@ -406,7 +406,7 @@ void test_unordered_map() sink(m28); // $ MISSING: ast,ir // additional try_emplace test cases - std::unordered_map> m29, m30, m31, m32; + std::unordered_map> m29, m30, m31, m32; sink(m29.try_emplace("abc", 1, 2)); sink(m29); sink(m29["abc"]); @@ -421,19 +421,19 @@ void test_unordered_map() sink(m32["abc"]); // $ ast,ir // additional emplace test cases - std::unordered_map m33; + std::unordered_map m33; sink(m33.emplace(source(), "def").first); // $ MISSING: ast,ir sink(m33); // $ MISSING: ast,ir - std::unordered_map m34, m35; - sink(m34.emplace(std::pair("abc", "def")).first); + std::unordered_map m34, m35; + sink(m34.emplace(std::pair("abc", "def")).first); sink(m34); - sink(m34.emplace(std::pair("abc", source())).first); + sink(m34.emplace(std::pair("abc", source())).first); sink(m34); // $ ast,ir sink(m34.emplace_hint(m34.begin(), "abc", "def")); // $ ast,ir sink(m35.emplace().first); sink(m35); - sink(m35.emplace(std::pair(source(), "def")).first); // $ MISSING: ast,ir + sink(m35.emplace(std::pair(source(), "def")).first); // $ MISSING: ast,ir sink(m35); // $ MISSING: ast,ir } From b63ce1d3e6750326d328e23c19ac596289d28453 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Mon, 12 Oct 2026 13:27:38 +0100 Subject: [PATCH 020/140] C++: Make map in the azure test more realistic. --- .../dataflow/external-models/azure.cpp | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp b/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp index 14407a8b5bed..7b3ec1a5eb55 100644 --- a/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp +++ b/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp @@ -7,6 +7,20 @@ using uint32_t = unsigned int; namespace std { + template + struct pair { + typedef T1 first_type; + typedef T2 second_type; + + T1 first; + T2 second; + pair(); + pair(const T1& x, const T2& y); + template pair(const pair &p); + + void swap(pair& p) /*noexcept(...)*/; + }; + class string { public: @@ -19,10 +33,12 @@ namespace std class map { public: + using key_type = K; + using value_type = pair; map(); ~map(); - V& operator[](const K& key); + V& operator[](const key_type& key); }; template From f9e4a41b7685615052c007fd3432ec8b5e618cf1 Mon Sep 17 00:00:00 2001 From: Mathias Vorreiter Pedersen Date: Mon, 12 Oct 2026 13:27:57 +0100 Subject: [PATCH 021/140] C++: Make strings in the azure test more realistic. --- .../dataflow/external-models/azure.cpp | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp b/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp index 7b3ec1a5eb55..5825297cab17 100644 --- a/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp +++ b/cpp/ql/test/library-tests/dataflow/external-models/azure.cpp @@ -21,13 +21,28 @@ namespace std void swap(pair& p) /*noexcept(...)*/; }; - class string - { - public: - string(); - string(const char *); - ~string(); - }; + template struct char_traits; + + typedef size_t streamsize; + + template class allocator { + public: + allocator() throw(); + typedef size_t size_type; + }; + + template, class Allocator = allocator > + class basic_string { + public: + using value_type = charT; + using reference = value_type&; + using const_reference = const value_type&; + typedef typename Allocator::size_type size_type; + + basic_string(const charT* s, const Allocator& a = Allocator()); + }; + + typedef basic_string string; template class map From bccfc9419abe50439159162eef6fe5e3d90fe58f Mon Sep 17 00:00:00 2001 From: Chaitanya Mangalagiri Date: Sat, 24 Oct 2026 16:12:40 -0700 Subject: [PATCH 022/140] Rust: Address unsafe deserialization review feedback Fix canonical deserialization model paths and JSON string payload handling. Add model and query-help regression coverage with generated expected results, mark known false positives, and correct query-help references. --- .../codeql/rust/frameworks/serde.model.yml | 27 +- .../CWE-502/UnsafeDeserialization.qhelp | 3 +- .../CWE-502/UnsafeDeserialization.expected | 232 ++++++++++++++++++ .../test/query-tests/security/CWE-502/main.rs | 80 +++++- .../query-tests/security/CWE-502/options.yml | 4 + 5 files changed, 330 insertions(+), 16 deletions(-) create mode 100644 rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected diff --git a/rust/ql/lib/codeql/rust/frameworks/serde.model.yml b/rust/ql/lib/codeql/rust/frameworks/serde.model.yml index 3d3c8d63ef85..4d1f2655f07d 100644 --- a/rust/ql/lib/codeql/rust/frameworks/serde.model.yml +++ b/rust/ql/lib/codeql/rust/frameworks/serde.model.yml @@ -4,21 +4,22 @@ extensions: extensible: sinkModel data: # serde_json deserialization functions - - ["crate::serde_json::from_str", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::serde_json::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::serde_json::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::serde_json::from_value", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_json::de::from_str", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_json::de::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_json::de::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_json::value::from_value", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_json::value::from_value", "Argument[0].Field[serde_json::value::Value::String(0)]", "unsafe-deserialization", "manual"] # bincode deserialization functions - - ["crate::bincode::deserialize", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::bincode::deserialize_from", "Argument[0]", "unsafe-deserialization", "manual"] + - ["bincode::deserialize", "Argument[0]", "unsafe-deserialization", "manual"] + - ["bincode::deserialize_from", "Argument[0]", "unsafe-deserialization", "manual"] # rmp_serde (MessagePack) deserialization functions - - ["crate::rmp_serde::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::rmp_serde::from_read", "Argument[0]", "unsafe-deserialization", "manual"] + - ["rmp_serde::decode::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] + - ["rmp_serde::decode::from_read", "Argument[0]", "unsafe-deserialization", "manual"] # ciborium (CBOR) deserialization functions - - ["crate::ciborium::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] + - ["ciborium::de::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] # serde_yaml deserialization functions - - ["crate::serde_yaml::from_str", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::serde_yaml::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] - - ["crate::serde_yaml::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_yaml::de::from_str", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_yaml::de::from_slice", "Argument[0]", "unsafe-deserialization", "manual"] + - ["serde_yaml::de::from_reader", "Argument[0]", "unsafe-deserialization", "manual"] # toml deserialization - - ["crate::toml::from_str", "Argument[0]", "unsafe-deserialization", "manual"] + - ["toml::de::from_str", "Argument[0]", "unsafe-deserialization", "manual"] diff --git a/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp b/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp index daa155b51fa9..87d3679b8933 100644 --- a/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp +++ b/rust/ql/src/queries/security/CWE-502/UnsafeDeserialization.qhelp @@ -39,8 +39,7 @@ A safer approach validates the input size and uses strict deserialization settin -
  • OWASP: Deserialization of untrusted data.
  • -
  • CWE-502: Deserialization of Untrusted Data.
  • +
  • OWASP: Deserialization of untrusted data.
  • diff --git a/rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected b/rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected new file mode 100644 index 000000000000..95e11221ac7d --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-502/UnsafeDeserialization.expected @@ -0,0 +1,232 @@ +#select +| main.rs:33:53:33:65 | &remote_bytes | main.rs:15:24:15:68 | ...::get(...) | main.rs:33:53:33:65 | &remote_bytes | This deserialization operation processes $@ without validation. | main.rs:15:24:15:68 | ...::get(...) | user-provided data | +| main.rs:36:51:36:64 | &remote_string | main.rs:19:25:19:69 | ...::get(...) | main.rs:36:51:36:64 | &remote_string | This deserialization operation processes $@ without validation. | main.rs:19:25:19:69 | ...::get(...) | user-provided data | +| main.rs:46:48:46:60 | &remote_bytes | main.rs:40:24:40:72 | ...::get(...) | main.rs:46:48:46:60 | &remote_bytes | This deserialization operation processes $@ without validation. | main.rs:40:24:40:72 | ...::get(...) | user-provided data | +| main.rs:58:52:58:65 | &remote_string | main.rs:50:25:50:69 | ...::get(...) | main.rs:58:52:58:65 | &remote_string | This deserialization operation processes $@ without validation. | main.rs:50:25:50:69 | ...::get(...) | user-provided data | +| main.rs:72:51:72:67 | &... | main.rs:63:24:63:72 | ...::get(...) | main.rs:72:51:72:67 | &... | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:73:54:73:70 | &... | main.rs:63:24:63:72 | ...::get(...) | main.rs:73:54:73:70 | &... | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:74:56:74:68 | &remote_bytes | main.rs:63:24:63:72 | ...::get(...) | main.rs:74:56:74:68 | &remote_bytes | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:75:56:75:72 | &... | main.rs:63:24:63:72 | ...::get(...) | main.rs:75:56:75:72 | &... | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:76:47:76:63 | &... | main.rs:63:24:63:72 | ...::get(...) | main.rs:76:47:76:63 | &... | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:77:50:77:63 | &remote_string | main.rs:67:25:67:73 | ...::get(...) | main.rs:77:50:77:63 | &remote_string | This deserialization operation processes $@ without validation. | main.rs:67:25:67:73 | ...::get(...) | user-provided data | +| main.rs:78:54:78:66 | &remote_bytes | main.rs:63:24:63:72 | ...::get(...) | main.rs:78:54:78:66 | &remote_bytes | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:79:56:79:72 | &... | main.rs:63:24:63:72 | ...::get(...) | main.rs:79:56:79:72 | &... | This deserialization operation processes $@ without validation. | main.rs:63:24:63:72 | ...::get(...) | user-provided data | +| main.rs:80:40:80:53 | &remote_string | main.rs:67:25:67:73 | ...::get(...) | main.rs:80:40:80:53 | &remote_string | This deserialization operation processes $@ without validation. | main.rs:67:25:67:73 | ...::get(...) | user-provided data | +| main.rs:81:54:81:93 | ...::String(...) | main.rs:67:25:67:73 | ...::get(...) | main.rs:81:54:81:93 | ...::String(...) | This deserialization operation processes $@ without validation. | main.rs:67:25:67:73 | ...::get(...) | user-provided data | +| main.rs:95:32:95:35 | body | main.rs:133:24:133:72 | ...::get(...) | main.rs:95:32:95:35 | body | This deserialization operation processes $@ without validation. | main.rs:133:24:133:72 | ...::get(...) | user-provided data | +| main.rs:128:32:128:35 | body | main.rs:133:24:133:72 | ...::get(...) | main.rs:128:32:128:35 | body | This deserialization operation processes $@ without validation. | main.rs:133:24:133:72 | ...::get(...) | user-provided data | +edges +| main.rs:15:9:15:20 | remote_bytes | main.rs:33:54:33:65 | remote_bytes | provenance | | +| main.rs:15:24:15:68 | ...::get(...) | main.rs:15:24:15:68 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:15:24:15:68 | ...::get(...) [Ok] | main.rs:15:24:16:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:15:24:16:17 | ... .unwrap() | main.rs:15:24:17:16 | ... .bytes() [Ok] | provenance | MaD:18 | +| main.rs:15:24:17:16 | ... .bytes() [Ok] | main.rs:15:24:18:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:15:24:18:17 | ... .unwrap() | main.rs:15:9:15:20 | remote_bytes | provenance | | +| main.rs:19:9:19:21 | remote_string | main.rs:36:52:36:64 | remote_string | provenance | | +| main.rs:19:25:19:69 | ...::get(...) | main.rs:19:25:19:69 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:19:25:19:69 | ...::get(...) [Ok] | main.rs:19:25:20:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:19:25:20:17 | ... .unwrap() | main.rs:19:25:21:15 | ... .text() [Ok] | provenance | MaD:19 | +| main.rs:19:25:21:15 | ... .text() [Ok] | main.rs:19:25:22:38 | ... .unwrap_or(...) | provenance | MaD:17 | +| main.rs:19:25:22:38 | ... .unwrap_or(...) | main.rs:19:9:19:21 | remote_string | provenance | | +| main.rs:33:53:33:65 | &remote_bytes [&ref] | main.rs:33:53:33:65 | &remote_bytes | provenance | Sink:MaD:7 | +| main.rs:33:54:33:65 | remote_bytes | main.rs:33:53:33:65 | &remote_bytes [&ref] | provenance | | +| main.rs:36:51:36:64 | &remote_string [&ref] | main.rs:36:51:36:64 | &remote_string | provenance | Sink:MaD:8 | +| main.rs:36:52:36:64 | remote_string | main.rs:36:51:36:64 | &remote_string [&ref] | provenance | | +| main.rs:40:9:40:20 | remote_bytes | main.rs:46:49:46:60 | remote_bytes | provenance | | +| main.rs:40:24:40:72 | ...::get(...) | main.rs:40:24:40:72 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:40:24:40:72 | ...::get(...) [Ok] | main.rs:40:24:41:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:40:24:41:17 | ... .unwrap() | main.rs:40:24:42:16 | ... .bytes() [Ok] | provenance | MaD:18 | +| main.rs:40:24:42:16 | ... .bytes() [Ok] | main.rs:40:24:43:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:40:24:43:17 | ... .unwrap() | main.rs:40:9:40:20 | remote_bytes | provenance | | +| main.rs:46:48:46:60 | &remote_bytes [&ref] | main.rs:46:48:46:60 | &remote_bytes | provenance | Sink:MaD:1 | +| main.rs:46:49:46:60 | remote_bytes | main.rs:46:48:46:60 | &remote_bytes [&ref] | provenance | | +| main.rs:50:9:50:21 | remote_string | main.rs:58:53:58:65 | remote_string | provenance | | +| main.rs:50:25:50:69 | ...::get(...) | main.rs:50:25:50:69 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:50:25:50:69 | ...::get(...) [Ok] | main.rs:50:25:51:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:50:25:51:17 | ... .unwrap() | main.rs:50:25:52:15 | ... .text() [Ok] | provenance | MaD:19 | +| main.rs:50:25:52:15 | ... .text() [Ok] | main.rs:50:25:53:38 | ... .unwrap_or(...) | provenance | MaD:17 | +| main.rs:50:25:53:38 | ... .unwrap_or(...) | main.rs:50:9:50:21 | remote_string | provenance | | +| main.rs:58:52:58:65 | &remote_string [&ref] | main.rs:58:52:58:65 | &remote_string | provenance | Sink:MaD:8 | +| main.rs:58:53:58:65 | remote_string | main.rs:58:52:58:65 | &remote_string [&ref] | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:72:52:72:63 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:72:52:72:67 | remote_bytes[...] | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:73:55:73:66 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:73:55:73:70 | remote_bytes[...] | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:74:57:74:68 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:75:57:75:68 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:75:57:75:72 | remote_bytes[...] | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:76:48:76:59 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:76:48:76:63 | remote_bytes[...] | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:78:55:78:66 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:79:57:79:68 | remote_bytes | provenance | | +| main.rs:63:9:63:20 | remote_bytes | main.rs:79:57:79:72 | remote_bytes[...] | provenance | | +| main.rs:63:24:63:72 | ...::get(...) | main.rs:63:24:63:72 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:63:24:63:72 | ...::get(...) [Ok] | main.rs:63:24:64:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:63:24:64:17 | ... .unwrap() | main.rs:63:24:65:16 | ... .bytes() [Ok] | provenance | MaD:18 | +| main.rs:63:24:65:16 | ... .bytes() [Ok] | main.rs:63:24:66:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:63:24:66:17 | ... .unwrap() | main.rs:63:9:63:20 | remote_bytes | provenance | | +| main.rs:67:9:67:21 | remote_string | main.rs:77:51:77:63 | remote_string | provenance | | +| main.rs:67:9:67:21 | remote_string | main.rs:80:41:80:53 | remote_string | provenance | | +| main.rs:67:9:67:21 | remote_string | main.rs:81:80:81:92 | remote_string | provenance | | +| main.rs:67:25:67:73 | ...::get(...) | main.rs:67:25:67:73 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:67:25:67:73 | ...::get(...) [Ok] | main.rs:67:25:68:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:67:25:68:17 | ... .unwrap() | main.rs:67:25:69:15 | ... .text() [Ok] | provenance | MaD:19 | +| main.rs:67:25:69:15 | ... .text() [Ok] | main.rs:67:25:70:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:67:25:70:17 | ... .unwrap() | main.rs:67:9:67:21 | remote_string | provenance | | +| main.rs:72:51:72:67 | &... [&ref] | main.rs:72:51:72:67 | &... | provenance | Sink:MaD:6 | +| main.rs:72:52:72:63 | remote_bytes | main.rs:72:52:72:67 | remote_bytes[...] | provenance | MaD:15 | +| main.rs:72:52:72:67 | remote_bytes[...] | main.rs:72:51:72:67 | &... [&ref] | provenance | | +| main.rs:73:54:73:70 | &... [&ref] | main.rs:73:54:73:70 | &... | provenance | Sink:MaD:2 | +| main.rs:73:55:73:66 | remote_bytes | main.rs:73:55:73:70 | remote_bytes[...] | provenance | MaD:15 | +| main.rs:73:55:73:70 | remote_bytes[...] | main.rs:73:54:73:70 | &... [&ref] | provenance | | +| main.rs:74:56:74:68 | &remote_bytes [&ref] | main.rs:74:56:74:68 | &remote_bytes | provenance | Sink:MaD:5 | +| main.rs:74:57:74:68 | remote_bytes | main.rs:74:56:74:68 | &remote_bytes [&ref] | provenance | | +| main.rs:75:56:75:72 | &... [&ref] | main.rs:75:56:75:72 | &... | provenance | Sink:MaD:4 | +| main.rs:75:57:75:68 | remote_bytes | main.rs:75:57:75:72 | remote_bytes[...] | provenance | MaD:15 | +| main.rs:75:57:75:72 | remote_bytes[...] | main.rs:75:56:75:72 | &... [&ref] | provenance | | +| main.rs:76:47:76:63 | &... [&ref] | main.rs:76:47:76:63 | &... | provenance | Sink:MaD:3 | +| main.rs:76:48:76:59 | remote_bytes | main.rs:76:48:76:63 | remote_bytes[...] | provenance | MaD:15 | +| main.rs:76:48:76:63 | remote_bytes[...] | main.rs:76:47:76:63 | &... [&ref] | provenance | | +| main.rs:77:50:77:63 | &remote_string [&ref] | main.rs:77:50:77:63 | &remote_string | provenance | Sink:MaD:12 | +| main.rs:77:51:77:63 | remote_string | main.rs:77:50:77:63 | &remote_string [&ref] | provenance | | +| main.rs:78:54:78:66 | &remote_bytes [&ref] | main.rs:78:54:78:66 | &remote_bytes | provenance | Sink:MaD:11 | +| main.rs:78:55:78:66 | remote_bytes | main.rs:78:54:78:66 | &remote_bytes [&ref] | provenance | | +| main.rs:79:56:79:72 | &... [&ref] | main.rs:79:56:79:72 | &... | provenance | Sink:MaD:10 | +| main.rs:79:57:79:68 | remote_bytes | main.rs:79:57:79:72 | remote_bytes[...] | provenance | MaD:15 | +| main.rs:79:57:79:72 | remote_bytes[...] | main.rs:79:56:79:72 | &... [&ref] | provenance | | +| main.rs:80:40:80:53 | &remote_string [&ref] | main.rs:80:40:80:53 | &remote_string | provenance | Sink:MaD:13 | +| main.rs:80:41:80:53 | remote_string | main.rs:80:40:80:53 | &remote_string [&ref] | provenance | | +| main.rs:81:54:81:93 | ...::String(...) [String] | main.rs:81:54:81:93 | ...::String(...) | provenance | Sink:MaD:9 | +| main.rs:81:80:81:92 | remote_string | main.rs:81:54:81:93 | ...::String(...) [String] | provenance | | +| main.rs:93:27:93:37 | ...: ... [&ref] | main.rs:95:32:95:35 | body | provenance | Sink:MaD:7 | +| main.rs:123:27:123:37 | ...: ... [&ref] | main.rs:128:32:128:35 | body | provenance | Sink:MaD:7 | +| main.rs:133:9:133:20 | remote_bytes | main.rs:142:48:142:59 | remote_bytes | provenance | | +| main.rs:133:9:133:20 | remote_bytes | main.rs:143:49:143:60 | remote_bytes | provenance | | +| main.rs:133:24:133:72 | ...::get(...) | main.rs:133:24:133:72 | ...::get(...) [Ok] | provenance | Src:MaD:14 | +| main.rs:133:24:133:72 | ...::get(...) [Ok] | main.rs:133:24:134:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:133:24:134:17 | ... .unwrap() | main.rs:133:24:135:16 | ... .bytes() [Ok] | provenance | MaD:18 | +| main.rs:133:24:135:16 | ... .bytes() [Ok] | main.rs:133:24:136:17 | ... .unwrap() | provenance | MaD:16 | +| main.rs:133:24:136:17 | ... .unwrap() | main.rs:133:9:133:20 | remote_bytes | provenance | | +| main.rs:142:47:142:59 | &remote_bytes [&ref] | main.rs:93:27:93:37 | ...: ... [&ref] | provenance | | +| main.rs:142:48:142:59 | remote_bytes | main.rs:142:47:142:59 | &remote_bytes [&ref] | provenance | | +| main.rs:143:48:143:60 | &remote_bytes [&ref] | main.rs:123:27:123:37 | ...: ... [&ref] | provenance | | +| main.rs:143:49:143:60 | remote_bytes | main.rs:143:48:143:60 | &remote_bytes [&ref] | provenance | | +models +| 1 | Sink: bincode::deserialize; Argument[0]; unsafe-deserialization | +| 2 | Sink: bincode::deserialize_from; Argument[0]; unsafe-deserialization | +| 3 | Sink: ciborium::de::from_reader; Argument[0]; unsafe-deserialization | +| 4 | Sink: rmp_serde::decode::from_read; Argument[0]; unsafe-deserialization | +| 5 | Sink: rmp_serde::decode::from_slice; Argument[0]; unsafe-deserialization | +| 6 | Sink: serde_json::de::from_reader; Argument[0]; unsafe-deserialization | +| 7 | Sink: serde_json::de::from_slice; Argument[0]; unsafe-deserialization | +| 8 | Sink: serde_json::de::from_str; Argument[0]; unsafe-deserialization | +| 9 | Sink: serde_json::value::from_value; Argument[0].Field[serde_json::value::Value::String(0)]; unsafe-deserialization | +| 10 | Sink: serde_yaml::de::from_reader; Argument[0]; unsafe-deserialization | +| 11 | Sink: serde_yaml::de::from_slice; Argument[0]; unsafe-deserialization | +| 12 | Sink: serde_yaml::de::from_str; Argument[0]; unsafe-deserialization | +| 13 | Sink: toml::de::from_str; Argument[0]; unsafe-deserialization | +| 14 | Source: reqwest::blocking::get; ReturnValue.Field[core::result::Result::Ok(0)]; remote | +| 15 | Summary: ::deref; Argument[self].Reference; ReturnValue.Reference; taint | +| 16 | Summary: ::unwrap; Argument[self].Field[core::result::Result::Ok(0)]; ReturnValue; value | +| 17 | Summary: ::unwrap_or; Argument[self].Field[core::result::Result::Ok(0)]; ReturnValue; value | +| 18 | Summary: ::bytes; Argument[self]; ReturnValue.Field[core::result::Result::Ok(0)]; taint | +| 19 | Summary: ::text; Argument[self]; ReturnValue.Field[core::result::Result::Ok(0)]; taint | +nodes +| main.rs:15:9:15:20 | remote_bytes | semmle.label | remote_bytes | +| main.rs:15:24:15:68 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:15:24:15:68 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:15:24:16:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:15:24:17:16 | ... .bytes() [Ok] | semmle.label | ... .bytes() [Ok] | +| main.rs:15:24:18:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:19:9:19:21 | remote_string | semmle.label | remote_string | +| main.rs:19:25:19:69 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:19:25:19:69 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:19:25:20:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:19:25:21:15 | ... .text() [Ok] | semmle.label | ... .text() [Ok] | +| main.rs:19:25:22:38 | ... .unwrap_or(...) | semmle.label | ... .unwrap_or(...) | +| main.rs:33:53:33:65 | &remote_bytes | semmle.label | &remote_bytes | +| main.rs:33:53:33:65 | &remote_bytes [&ref] | semmle.label | &remote_bytes [&ref] | +| main.rs:33:54:33:65 | remote_bytes | semmle.label | remote_bytes | +| main.rs:36:51:36:64 | &remote_string | semmle.label | &remote_string | +| main.rs:36:51:36:64 | &remote_string [&ref] | semmle.label | &remote_string [&ref] | +| main.rs:36:52:36:64 | remote_string | semmle.label | remote_string | +| main.rs:40:9:40:20 | remote_bytes | semmle.label | remote_bytes | +| main.rs:40:24:40:72 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:40:24:40:72 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:40:24:41:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:40:24:42:16 | ... .bytes() [Ok] | semmle.label | ... .bytes() [Ok] | +| main.rs:40:24:43:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:46:48:46:60 | &remote_bytes | semmle.label | &remote_bytes | +| main.rs:46:48:46:60 | &remote_bytes [&ref] | semmle.label | &remote_bytes [&ref] | +| main.rs:46:49:46:60 | remote_bytes | semmle.label | remote_bytes | +| main.rs:50:9:50:21 | remote_string | semmle.label | remote_string | +| main.rs:50:25:50:69 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:50:25:50:69 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:50:25:51:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:50:25:52:15 | ... .text() [Ok] | semmle.label | ... .text() [Ok] | +| main.rs:50:25:53:38 | ... .unwrap_or(...) | semmle.label | ... .unwrap_or(...) | +| main.rs:58:52:58:65 | &remote_string | semmle.label | &remote_string | +| main.rs:58:52:58:65 | &remote_string [&ref] | semmle.label | &remote_string [&ref] | +| main.rs:58:53:58:65 | remote_string | semmle.label | remote_string | +| main.rs:63:9:63:20 | remote_bytes | semmle.label | remote_bytes | +| main.rs:63:24:63:72 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:63:24:63:72 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:63:24:64:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:63:24:65:16 | ... .bytes() [Ok] | semmle.label | ... .bytes() [Ok] | +| main.rs:63:24:66:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:67:9:67:21 | remote_string | semmle.label | remote_string | +| main.rs:67:25:67:73 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:67:25:67:73 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:67:25:68:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:67:25:69:15 | ... .text() [Ok] | semmle.label | ... .text() [Ok] | +| main.rs:67:25:70:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:72:51:72:67 | &... | semmle.label | &... | +| main.rs:72:51:72:67 | &... [&ref] | semmle.label | &... [&ref] | +| main.rs:72:52:72:63 | remote_bytes | semmle.label | remote_bytes | +| main.rs:72:52:72:67 | remote_bytes[...] | semmle.label | remote_bytes[...] | +| main.rs:73:54:73:70 | &... | semmle.label | &... | +| main.rs:73:54:73:70 | &... [&ref] | semmle.label | &... [&ref] | +| main.rs:73:55:73:66 | remote_bytes | semmle.label | remote_bytes | +| main.rs:73:55:73:70 | remote_bytes[...] | semmle.label | remote_bytes[...] | +| main.rs:74:56:74:68 | &remote_bytes | semmle.label | &remote_bytes | +| main.rs:74:56:74:68 | &remote_bytes [&ref] | semmle.label | &remote_bytes [&ref] | +| main.rs:74:57:74:68 | remote_bytes | semmle.label | remote_bytes | +| main.rs:75:56:75:72 | &... | semmle.label | &... | +| main.rs:75:56:75:72 | &... [&ref] | semmle.label | &... [&ref] | +| main.rs:75:57:75:68 | remote_bytes | semmle.label | remote_bytes | +| main.rs:75:57:75:72 | remote_bytes[...] | semmle.label | remote_bytes[...] | +| main.rs:76:47:76:63 | &... | semmle.label | &... | +| main.rs:76:47:76:63 | &... [&ref] | semmle.label | &... [&ref] | +| main.rs:76:48:76:59 | remote_bytes | semmle.label | remote_bytes | +| main.rs:76:48:76:63 | remote_bytes[...] | semmle.label | remote_bytes[...] | +| main.rs:77:50:77:63 | &remote_string | semmle.label | &remote_string | +| main.rs:77:50:77:63 | &remote_string [&ref] | semmle.label | &remote_string [&ref] | +| main.rs:77:51:77:63 | remote_string | semmle.label | remote_string | +| main.rs:78:54:78:66 | &remote_bytes | semmle.label | &remote_bytes | +| main.rs:78:54:78:66 | &remote_bytes [&ref] | semmle.label | &remote_bytes [&ref] | +| main.rs:78:55:78:66 | remote_bytes | semmle.label | remote_bytes | +| main.rs:79:56:79:72 | &... | semmle.label | &... | +| main.rs:79:56:79:72 | &... [&ref] | semmle.label | &... [&ref] | +| main.rs:79:57:79:68 | remote_bytes | semmle.label | remote_bytes | +| main.rs:79:57:79:72 | remote_bytes[...] | semmle.label | remote_bytes[...] | +| main.rs:80:40:80:53 | &remote_string | semmle.label | &remote_string | +| main.rs:80:40:80:53 | &remote_string [&ref] | semmle.label | &remote_string [&ref] | +| main.rs:80:41:80:53 | remote_string | semmle.label | remote_string | +| main.rs:81:54:81:93 | ...::String(...) | semmle.label | ...::String(...) | +| main.rs:81:54:81:93 | ...::String(...) [String] | semmle.label | ...::String(...) [String] | +| main.rs:81:80:81:92 | remote_string | semmle.label | remote_string | +| main.rs:93:27:93:37 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:95:32:95:35 | body | semmle.label | body | +| main.rs:123:27:123:37 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:128:32:128:35 | body | semmle.label | body | +| main.rs:133:9:133:20 | remote_bytes | semmle.label | remote_bytes | +| main.rs:133:24:133:72 | ...::get(...) | semmle.label | ...::get(...) | +| main.rs:133:24:133:72 | ...::get(...) [Ok] | semmle.label | ...::get(...) [Ok] | +| main.rs:133:24:134:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:133:24:135:16 | ... .bytes() [Ok] | semmle.label | ... .bytes() [Ok] | +| main.rs:133:24:136:17 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:142:47:142:59 | &remote_bytes [&ref] | semmle.label | &remote_bytes [&ref] | +| main.rs:142:48:142:59 | remote_bytes | semmle.label | remote_bytes | +| main.rs:143:48:143:60 | &remote_bytes [&ref] | semmle.label | &remote_bytes [&ref] | +| main.rs:143:49:143:60 | remote_bytes | semmle.label | remote_bytes | +subpaths diff --git a/rust/ql/test/query-tests/security/CWE-502/main.rs b/rust/ql/test/query-tests/security/CWE-502/main.rs index 5c607982c36b..e5f860bccddf 100644 --- a/rust/ql/test/query-tests/security/CWE-502/main.rs +++ b/rust/ql/test/query-tests/security/CWE-502/main.rs @@ -55,12 +55,90 @@ fn test_safe_with_validation() { // Safe: size check before deserialization (still flagged as the barrier // is not modeled as a data flow barrier, but demonstrates the pattern) if remote_string.len() < 1024 { - let _data: UserData = serde_json::from_str(&remote_string).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote4 + let _data: UserData = serde_json::from_str(&remote_string).unwrap(); // $ SPURIOUS: Alert[rust/unsafe-deserialization]=remote4 + } +} + +fn test_other_deserialization_models() { + let remote_bytes = reqwest::blocking::get("http://example.com/data") // $ Source=remote5 + .unwrap() + .bytes() + .unwrap(); + let remote_string = reqwest::blocking::get("http://example.com/data") // $ Source=remote6 + .unwrap() + .text() + .unwrap(); + + let _json: UserData = serde_json::from_reader(&remote_bytes[..]).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _bincode: Config = bincode::deserialize_from(&remote_bytes[..]).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _msgpack_slice: Config = rmp_serde::from_slice(&remote_bytes).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _msgpack_reader: Config = rmp_serde::from_read(&remote_bytes[..]).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _cbor: Config = ciborium::from_reader(&remote_bytes[..]).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _yaml_str: Config = serde_yaml::from_str(&remote_string).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote6 + let _yaml_slice: Config = serde_yaml::from_slice(&remote_bytes).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _yaml_reader: Config = serde_yaml::from_reader(&remote_bytes[..]).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote5 + let _toml: Config = toml::from_str(&remote_string).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote6 + let _json_value: String = serde_json::from_value(serde_json::Value::String(remote_string)).unwrap(); // $ Alert[rust/unsafe-deserialization]=remote6 +} + +mod qhelp_example_bad { + use serde::Deserialize; + + #[derive(Deserialize)] + pub struct UserData { + name: String, + items: Vec, + } + + pub fn handle_request(body: &[u8]) -> UserData { + // BAD: deserializing user-controlled data without size validation + serde_json::from_slice(body).unwrap() // $ Alert[rust/unsafe-deserialization]=remote7 + } +} + +mod qhelp_example_good { + use serde::Deserialize; + + const MAX_BODY_SIZE: usize = 1024 * 1024; // 1 MB limit + + #[derive(Deserialize)] + #[serde(deny_unknown_fields)] + pub struct UserData { + name: String, + #[serde(deserialize_with = "bounded_vec")] + items: Vec, + } + + fn bounded_vec<'de, D>(deserializer: D) -> Result, D::Error> + where + D: serde::Deserializer<'de>, + { + let v = Vec::::deserialize(deserializer)?; + if v.len() > 100 { + return Err(serde::de::Error::custom("too many items")); + } + Ok(v) + } + + pub fn handle_request(body: &[u8]) -> Result { + // GOOD: validate input size before deserialization, use bounded containers + if body.len() > MAX_BODY_SIZE { + return Err("payload too large".to_string()); + } + serde_json::from_slice(body).map_err(|e| e.to_string()) // $ SPURIOUS: Alert[rust/unsafe-deserialization]=remote7 } } fn main() { + let remote_bytes = reqwest::blocking::get("http://example.com/data") // $ Source=remote7 + .unwrap() + .bytes() + .unwrap(); + test_serde_json_deserialization(); test_bincode_deserialization(); test_safe_with_validation(); + test_other_deserialization_models(); + let _ = qhelp_example_bad::handle_request(&remote_bytes); + let _ = qhelp_example_good::handle_request(&remote_bytes); } diff --git a/rust/ql/test/query-tests/security/CWE-502/options.yml b/rust/ql/test/query-tests/security/CWE-502/options.yml index f3664c3c1ba7..bbfa47291512 100644 --- a/rust/ql/test/query-tests/security/CWE-502/options.yml +++ b/rust/ql/test/query-tests/security/CWE-502/options.yml @@ -4,3 +4,7 @@ qltest_dependencies: - serde = { version = "1", features = ["derive"] } - serde_json = { version = "1" } - bincode = { version = "1" } + - rmp-serde = { version = "1" } + - ciborium = { version = "0.2" } + - serde_yaml = { version = "0.9" } + - toml = { version = "0.8" } From c0a00a9bd3dfe44f0f4fb1527bcc4b107149e2b7 Mon Sep 17 00:00:00 2001 From: Taus Date: Thu, 15 Oct 2026 12:04:04 +0000 Subject: [PATCH 023/140] yeast: Get rid of postfix `?` operator This will be superseded by the next commit, which generalised the mechanism and also makes the implementation simpler. --- shared/yeast-macros/src/ast.rs | 4 +- shared/yeast-macros/src/lib.rs | 27 ------ shared/yeast-macros/src/lower.rs | 101 ++++----------------- shared/yeast-macros/src/template_parse.rs | 60 +----------- shared/yeast/doc/yeast.md | 42 --------- shared/yeast/src/lib.rs | 79 +--------------- shared/yeast/tests/test.rs | 106 ---------------------- 7 files changed, 24 insertions(+), 395 deletions(-) diff --git a/shared/yeast-macros/src/ast.rs b/shared/yeast-macros/src/ast.rs index f400257fd1d4..09746b550235 100644 --- a/shared/yeast-macros/src/ast.rs +++ b/shared/yeast-macros/src/ast.rs @@ -121,15 +121,13 @@ pub(crate) enum Content { pub(crate) enum Field { /// A named field such as `argument: {args}` or - /// `label: (identifier #{name})?`. + /// `label: (identifier #{name})`. Named { /// The field name before `:`, such as `argument`. name: Ident, /// The node or Rust splice after `:`, such as `(identifier "x")` or /// `{args}`. value: Template, - /// Whether the field value has a trailing `?`. - optional: bool, }, /// Source-location metadata such as `location: {source}`. Location { diff --git a/shared/yeast-macros/src/lib.rs b/shared/yeast-macros/src/lib.rs index 75d737372965..aeb7b0a5df56 100644 --- a/shared/yeast-macros/src/lib.rs +++ b/shared/yeast-macros/src/lib.rs @@ -63,37 +63,10 @@ pub fn query(input: TokenStream) -> TokenStream { /// `Option`, iterator chains) splice /// their elements /// field: {expr} - extend a named field with `{expr}`'s ids -/// field: (kind ...)? - set the field only if every `#{expr}` -/// beneath it has a value (see below) /// location: {expr} - override this node's source range with the /// span of one or more nodes; may be repeated /// ``` /// -/// # Optional fields -/// -/// A `?` on a field's value makes that field fallible: if a `#{expr}` -/// anywhere beneath it interpolates an absent value — an `Option` that is -/// `None` — the subtree is abandoned and the field is left unset. This -/// replaces the surrounding `Option::map` that would otherwise be needed: -/// -/// ```text -/// (break_expr label: {lbl.map(|l| tree!((identifier #{l})))}) // before -/// (break_expr label: (identifier #{lbl})?) // after -/// ``` -/// -/// The value may be nested arbitrarily deeply, and a nested `?` catches -/// first, so an inner absent value need not discard the outer node: -/// -/// ```text -/// (parameter pattern: (name_pattern identifier: (identifier #{name}))? type: {ty}) -/// ``` -/// -/// Only `#{expr}` propagates absence. A `{expr}` splice is unaffected, since -/// yielding no ids already leaves a field unset, and `?` is rejected on one. -/// Outside a `?`, interpolating an `Option` with `#{expr}` remains a compile -/// error, so the choice between "unset the field" and "unwrap it" stays -/// explicit. -/// /// Uses the `BuildCtx` binding named `ctx` from the surrounding scope: /// /// ```text diff --git a/shared/yeast-macros/src/lower.rs b/shared/yeast-macros/src/lower.rs index 6bf22c7e8b9e..89bfdf0a6e76 100644 --- a/shared/yeast-macros/src/lower.rs +++ b/shared/yeast-macros/src/lower.rs @@ -1,21 +1,8 @@ use proc_macro2::{Ident, Span, TokenStream}; use quote::quote; -use std::sync::atomic::{AtomicUsize, Ordering}; -use syn::Lifetime; use crate::ast::{Cardinality, Content, Field, Node, Pattern, Replacement, Rule, Template}; -struct LoweringContext<'a> { - /// Enclosing optional-field label to exit when a computed value is absent. - fallible_scope: Option<&'a Lifetime>, -} - -fn fresh_fallible_label() -> Lifetime { - static COUNTER: AtomicUsize = AtomicUsize::new(0); - let n = COUNTER.fetch_add(1, Ordering::Relaxed); - Lifetime::new(&format!("'__yeast_field_{n}"), Span::call_site()) -} - #[derive(Clone)] struct CaptureBinding { name: Ident, @@ -286,20 +273,15 @@ fn capture_bindings<'a>( impl Template { pub(crate) fn lower_root(&self) -> TokenStream { - self.lower(&LoweringContext { - fallible_scope: None, - }) + self.lower() } pub(crate) fn lower_list(templates: &[Self]) -> TokenStream { - let context = LoweringContext { - fallible_scope: None, - }; let items = templates .iter() .map(|template| match template { Template::Node(node) => { - let node = node.lower(&context); + let node = node.lower(); quote! { __nodes.push(#node); } } Template::Splice(block) => { @@ -322,9 +304,9 @@ impl Template { } } - fn lower(&self, context: &LoweringContext<'_>) -> TokenStream { + fn lower(&self) -> TokenStream { match self { - Template::Node(node) => node.lower(context), + Template::Node(node) => node.lower(), Template::Splice(block) => { let block = rust_block(block); quote! { ::std::convert::Into::::into(#block) } @@ -334,7 +316,7 @@ impl Template { } impl Node { - fn lower(&self, context: &LoweringContext<'_>) -> TokenStream { + fn lower(&self) -> TokenStream { let ctx = Ident::new("ctx", Span::call_site()); let kind_str = self.kind.to_string(); @@ -343,40 +325,18 @@ impl Node { Content::Static(lit) => quote! { #ctx.literal(#kind_str, #lit) }, Content::Computed(block) => { let block = rust_block(block); - if let Some(label) = context.fallible_scope { - quote! { - { - let __expr = #block; - let ::std::option::Option::Some(__value_ref) = - yeast::MaybeYeastValue::maybe_yeast_value(&__expr) - else { - break #label ::std::option::Option::None; - }; - let __value = - yeast::YeastDisplay::yeast_to_string(__value_ref, &#ctx); - let __source_range = - yeast::YeastSourceRange::yeast_source_range(__value_ref, &#ctx); - #ctx.literal_with_source_range( - #kind_str, - &__value, - __source_range, - ) - } - } - } else { - quote! { - { - let __expr = #block; - let __value = - yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx); - let __source_range = - yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx); - #ctx.literal_with_source_range( - #kind_str, - &__value, - __source_range, - ) - } + quote! { + { + let __expr = #block; + let __value = + yeast::YeastDisplay::yeast_to_string(&__expr, &#ctx); + let __source_range = + yeast::YeastSourceRange::yeast_source_range(&__expr, &#ctx); + #ctx.literal_with_source_range( + #kind_str, + &__value, + __source_range, + ) } } } @@ -399,11 +359,7 @@ impl Node { #ctx.extend_location_ranges(__value, &mut __location_ranges); }); } - Field::Named { - name, - value, - optional, - } => { + Field::Named { name, value } => { let field_name = name.to_string(); let field_str = field_name .strip_prefix("r#") @@ -413,10 +369,8 @@ impl Node { &format!("__field_{field_str}_{field_counter}"), Span::call_site(), ); - match value { Template::Splice(block) => { - debug_assert!(!optional); let block = rust_block(block); stmts.push(quote! { let mut #temp: Vec = Vec::new(); @@ -430,25 +384,8 @@ impl Node { } }); } - Template::Node(child) if *optional => { - let label = fresh_fallible_label(); - let value = child.lower(&LoweringContext { - fallible_scope: Some(&label), - }); - stmts.push(quote! { - #[allow(unused_labels)] - let #temp: ::std::option::Option = #label: { - ::std::option::Option::Some(#value) - }; - }); - field_args.push(quote! { - if let ::std::option::Option::Some(__id) = #temp { - __fields.push((#field_str, vec![__id])); - } - }); - } Template::Node(child) => { - let value = child.lower(context); + let value = child.lower(); stmts.push(quote! { let #temp: yeast::OutputId = #value; }); field_args.push(quote! { __fields.push((#field_str, vec![#temp])); diff --git a/shared/yeast-macros/src/template_parse.rs b/shared/yeast-macros/src/template_parse.rs index 782407466a46..8a7d176809ef 100644 --- a/shared/yeast-macros/src/template_parse.rs +++ b/shared/yeast-macros/src/template_parse.rs @@ -9,7 +9,6 @@ type Result = std::result::Result; pub(crate) fn parse_template(input: TokenStream) -> Result