From 3a8bf46c2e2a554cb3d411c7d6c17c18a33b242b Mon Sep 17 00:00:00 2001 From: Chanel Young Date: Tue, 16 Jun 2026 10:49:54 -0700 Subject: [PATCH 1/2] rust: Exclude environment variables as XSS taint sources Instead of a name-based barrier on config-looking struct fields, stop treating the `environment` threat model (e.g. std::env::var) as a source for the XSS query. Environment variables hold trusted deployment configuration set at startup, not per-request attacker input. This fixes the false positive where opts.server.external_domain (loaded from the EXTERNAL_DOMAIN env var) was flagged as XSS in an HTML response, without the fragility of matching on field names. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: bf9c111e-51cd-4c12-aeaa-cdcf1592f0f7 --- .../2026-06-16-xss-exclude-environment-sources.md | 4 ++++ rust/ql/lib/codeql/rust/security/XssExtensions.qll | 9 ++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md diff --git a/rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md b/rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md new file mode 100644 index 000000000000..37988c96d1ec --- /dev/null +++ b/rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md @@ -0,0 +1,4 @@ +--- +category: minorAnalysis +--- +* The cross-site scripting (XSS) query no longer treats environment variables (the `environment` threat model, e.g. `std::env::var`) as a taint source. These values are trusted deployment configuration set at startup rather than per-request attacker-controlled input, so excluding them removes a class of false positives. diff --git a/rust/ql/lib/codeql/rust/security/XssExtensions.qll b/rust/ql/lib/codeql/rust/security/XssExtensions.qll index 74ed161acb09..79aa7346bac2 100644 --- a/rust/ql/lib/codeql/rust/security/XssExtensions.qll +++ b/rust/ql/lib/codeql/rust/security/XssExtensions.qll @@ -35,8 +35,15 @@ module Xss { /** * An active threat-model source, considered as a flow source. + * + * Environment variables are excluded: they hold trusted deployment + * configuration (for example the service's own hostname or public base URL, + * set from an environment variable at startup) rather than per-request, + * attacker-controlled input, so they are not a meaningful source for XSS. */ - private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { } + private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { + ActiveThreatModelSourceAsSource() { not this.getThreatModel() = "environment" } + } /** * A sink for XSS from model data. From 887dc254e4f68585cf3049edac0fdc19a66b7ccc Mon Sep 17 00:00:00 2001 From: dilanbhalla Date: Fri, 21 Aug 2026 12:43:10 -0700 Subject: [PATCH 2/2] rust: Narrow XSS config host barrier Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...6-06-16-xss-exclude-environment-sources.md | 4 -- .../codeql/rust/security/XssExtensions.qll | 24 +++++-- .../security/CWE-079/axum/XSS.expected | 70 ++++++++++++++----- .../query-tests/security/CWE-079/axum/main.rs | 27 ++++++- 4 files changed, 95 insertions(+), 30 deletions(-) delete mode 100644 rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md diff --git a/rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md b/rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md deleted file mode 100644 index 37988c96d1ec..000000000000 --- a/rust/ql/lib/change-notes/2026-06-16-xss-exclude-environment-sources.md +++ /dev/null @@ -1,4 +0,0 @@ ---- -category: minorAnalysis ---- -* The cross-site scripting (XSS) query no longer treats environment variables (the `environment` threat model, e.g. `std::env::var`) as a taint source. These values are trusted deployment configuration set at startup rather than per-request attacker-controlled input, so excluding them removes a class of false positives. diff --git a/rust/ql/lib/codeql/rust/security/XssExtensions.qll b/rust/ql/lib/codeql/rust/security/XssExtensions.qll index 79aa7346bac2..bcb560cf2f0e 100644 --- a/rust/ql/lib/codeql/rust/security/XssExtensions.qll +++ b/rust/ql/lib/codeql/rust/security/XssExtensions.qll @@ -35,14 +35,24 @@ module Xss { /** * An active threat-model source, considered as a flow source. - * - * Environment variables are excluded: they hold trusted deployment - * configuration (for example the service's own hostname or public base URL, - * set from an environment variable at startup) rather than per-request, - * attacker-controlled input, so they are not a meaningful source for XSS. */ - private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { - ActiveThreatModelSourceAsSource() { not this.getThreatModel() = "environment" } + private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { } + + /** + * A host or URL field read from a configuration type. + */ + private class ConfigHostFieldBarrier extends Barrier { + ConfigHostFieldBarrier() { + exists(FieldExpr field, Struct configType, string fieldName | + this.asExpr() = field and + field.getStructField().isStructField(configType, fieldName) and + configType.getName().getText().regexpMatch(".*(Config|Configuration|Options|Opts|Settings).*") and + fieldName = + [ + "external_domain", "hostname", "host_name", "base_url", "server_url", "public_url" + ] + ) + } } /** diff --git a/rust/ql/test/query-tests/security/CWE-079/axum/XSS.expected b/rust/ql/test/query-tests/security/CWE-079/axum/XSS.expected index 385ce0f58c48..7f5c92cce581 100644 --- a/rust/ql/test/query-tests/security/CWE-079/axum/XSS.expected +++ b/rust/ql/test/query-tests/security/CWE-079/axum/XSS.expected @@ -1,24 +1,58 @@ #select -| main.rs:10:10:10:21 | html_content | main.rs:15:51:15:53 | get | main.rs:10:10:10:21 | html_content | Cross-site scripting vulnerability due to a $@. | main.rs:15:51:15:53 | get | user-provided value | +| main.rs:15:10:15:21 | html_content | main.rs:37:26:37:28 | get | main.rs:15:10:15:21 | html_content | Cross-site scripting vulnerability due to a $@. | main.rs:37:26:37:28 | get | user-provided value | +| main.rs:19:10:19:60 | MacroExpr | main.rs:38:33:38:35 | get | main.rs:19:10:19:60 | MacroExpr | Cross-site scripting vulnerability due to a $@. | main.rs:38:33:38:35 | get | user-provided value | +| main.rs:31:10:31:34 | MacroExpr | main.rs:30:17:30:29 | ...::var | main.rs:31:10:31:34 | MacroExpr | Cross-site scripting vulnerability due to a $@. | main.rs:30:17:30:29 | ...::var | user-provided value | edges -| main.rs:8:24:8:59 | ...: Query::<...> | main.rs:9:32:9:63 | MacroExpr | provenance | | -| main.rs:9:9:9:20 | html_content | main.rs:10:10:10:21 | html_content | provenance | | -| main.rs:9:32:9:63 | ...::format(...) | main.rs:9:32:9:63 | { ... } | provenance | | -| main.rs:9:32:9:63 | ...::must_use(...) | main.rs:9:9:9:20 | html_content | provenance | | -| main.rs:9:32:9:63 | MacroExpr | main.rs:9:32:9:63 | ...::format(...) | provenance | MaD:2 | -| main.rs:9:32:9:63 | { ... } | main.rs:9:32:9:63 | ...::must_use(...) | provenance | MaD:3 | -| main.rs:15:51:15:53 | get | main.rs:8:24:8:59 | ...: Query::<...> | provenance | Src:MaD:1 | +| main.rs:13:24:13:59 | ...: Query::<...> | main.rs:14:32:14:63 | MacroExpr | provenance | | +| main.rs:14:9:14:20 | html_content | main.rs:15:10:15:21 | html_content | provenance | | +| main.rs:14:32:14:63 | ...::format(...) | main.rs:14:32:14:63 | { ... } | provenance | | +| main.rs:14:32:14:63 | ...::must_use(...) | main.rs:14:9:14:20 | html_content | provenance | | +| main.rs:14:32:14:63 | MacroExpr | main.rs:14:32:14:63 | ...::format(...) | provenance | MaD:4 | +| main.rs:14:32:14:63 | { ... } | main.rs:14:32:14:63 | ...::must_use(...) | provenance | MaD:5 | +| main.rs:18:31:18:66 | ...: Query::<...> | main.rs:19:18:19:59 | MacroExpr | provenance | | +| main.rs:19:18:19:59 | ...::format(...) | main.rs:19:18:19:59 | { ... } | provenance | | +| main.rs:19:18:19:59 | ...::must_use(...) | main.rs:19:10:19:60 | MacroExpr | provenance | | +| main.rs:19:18:19:59 | MacroExpr | main.rs:19:18:19:59 | ...::format(...) | provenance | MaD:4 | +| main.rs:19:18:19:59 | { ... } | main.rs:19:18:19:59 | ...::must_use(...) | provenance | MaD:5 | +| main.rs:30:9:30:13 | value | main.rs:31:18:31:33 | MacroExpr | provenance | | +| main.rs:30:17:30:29 | ...::var | main.rs:30:17:30:37 | ...::var(...) [Ok] | provenance | Src:MaD:2 | +| main.rs:30:17:30:37 | ...::var(...) [Ok] | main.rs:30:17:30:46 | ... .unwrap() | provenance | MaD:3 | +| main.rs:30:17:30:46 | ... .unwrap() | main.rs:30:9:30:13 | value | provenance | | +| main.rs:31:18:31:33 | ...::format(...) | main.rs:31:18:31:33 | { ... } | provenance | | +| main.rs:31:18:31:33 | ...::must_use(...) | main.rs:31:10:31:34 | MacroExpr | provenance | | +| main.rs:31:18:31:33 | MacroExpr | main.rs:31:18:31:33 | ...::format(...) | provenance | MaD:4 | +| main.rs:31:18:31:33 | { ... } | main.rs:31:18:31:33 | ...::must_use(...) | provenance | MaD:5 | +| main.rs:37:26:37:28 | get | main.rs:13:24:13:59 | ...: Query::<...> | provenance | Src:MaD:1 | +| main.rs:38:33:38:35 | get | main.rs:18:31:18:66 | ...: Query::<...> | provenance | Src:MaD:1 | models | 1 | Source: axum::routing::method_routing::get; Argument[0].Parameter[0..7]; remote | -| 2 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint | -| 3 | Summary: core::hint::must_use; Argument[0]; ReturnValue; value | +| 2 | Source: std::env::var; ReturnValue.Field[core::result::Result::Ok(0)]; environment | +| 3 | Summary: ::unwrap; Argument[self].Field[core::result::Result::Ok(0)]; ReturnValue; value | +| 4 | Summary: alloc::fmt::format; Argument[0]; ReturnValue; taint | +| 5 | Summary: core::hint::must_use; Argument[0]; ReturnValue; value | nodes -| main.rs:8:24:8:59 | ...: Query::<...> | semmle.label | ...: Query::<...> | -| main.rs:9:9:9:20 | html_content | semmle.label | html_content | -| main.rs:9:32:9:63 | ...::format(...) | semmle.label | ...::format(...) | -| main.rs:9:32:9:63 | ...::must_use(...) | semmle.label | ...::must_use(...) | -| main.rs:9:32:9:63 | MacroExpr | semmle.label | MacroExpr | -| main.rs:9:32:9:63 | { ... } | semmle.label | { ... } | -| main.rs:10:10:10:21 | html_content | semmle.label | html_content | -| main.rs:15:51:15:53 | get | semmle.label | get | +| main.rs:13:24:13:59 | ...: Query::<...> | semmle.label | ...: Query::<...> | +| main.rs:14:9:14:20 | html_content | semmle.label | html_content | +| main.rs:14:32:14:63 | ...::format(...) | semmle.label | ...::format(...) | +| main.rs:14:32:14:63 | ...::must_use(...) | semmle.label | ...::must_use(...) | +| main.rs:14:32:14:63 | MacroExpr | semmle.label | MacroExpr | +| main.rs:14:32:14:63 | { ... } | semmle.label | { ... } | +| main.rs:15:10:15:21 | html_content | semmle.label | html_content | +| main.rs:18:31:18:66 | ...: Query::<...> | semmle.label | ...: Query::<...> | +| main.rs:19:10:19:60 | MacroExpr | semmle.label | MacroExpr | +| main.rs:19:18:19:59 | ...::format(...) | semmle.label | ...::format(...) | +| main.rs:19:18:19:59 | ...::must_use(...) | semmle.label | ...::must_use(...) | +| main.rs:19:18:19:59 | MacroExpr | semmle.label | MacroExpr | +| main.rs:19:18:19:59 | { ... } | semmle.label | { ... } | +| main.rs:30:9:30:13 | value | semmle.label | value | +| main.rs:30:17:30:29 | ...::var | semmle.label | ...::var | +| main.rs:30:17:30:37 | ...::var(...) [Ok] | semmle.label | ...::var(...) [Ok] | +| main.rs:30:17:30:46 | ... .unwrap() | semmle.label | ... .unwrap() | +| main.rs:31:10:31:34 | MacroExpr | semmle.label | MacroExpr | +| main.rs:31:18:31:33 | ...::format(...) | semmle.label | ...::format(...) | +| main.rs:31:18:31:33 | ...::must_use(...) | semmle.label | ...::must_use(...) | +| main.rs:31:18:31:33 | MacroExpr | semmle.label | MacroExpr | +| main.rs:31:18:31:33 | { ... } | semmle.label | { ... } | +| main.rs:37:26:37:28 | get | semmle.label | get | +| main.rs:38:33:38:35 | get | semmle.label | get | subpaths diff --git a/rust/ql/test/query-tests/security/CWE-079/axum/main.rs b/rust/ql/test/query-tests/security/CWE-079/axum/main.rs index 27807c5883d9..add4a648f336 100644 --- a/rust/ql/test/query-tests/security/CWE-079/axum/main.rs +++ b/rust/ql/test/query-tests/security/CWE-079/axum/main.rs @@ -3,6 +3,11 @@ use axum::{extract::Query, response::Html, routing::get, Router}; #[derive(serde::Deserialize)] struct GreetingParams { name: String, + external_domain: String, +} + +struct ServerConfig { + external_domain: String, } async fn greet_handler(Query(params): Query) -> Html { @@ -10,9 +15,29 @@ async fn greet_handler(Query(params): Query) -> Html { Html(html_content) // $ Alert[rust/xss]=greet } +async fn request_host_handler(Query(params): Query) -> Html { + Html(format!("

Host: {}!

", params.external_domain)) // $ Alert[rust/xss]=requestHost +} + +async fn config_host_handler() -> Html { + let config = ServerConfig { + external_domain: std::env::var("EXTERNAL_DOMAIN").unwrap(), + }; + Html(format!("

Host: {}!

", config.external_domain)) +} + +async fn environment_handler() -> Html { + let value = std::env::var("HTML").unwrap(); // $ Source=environment + Html(format!("

{value}

")) // $ Alert[rust/xss]=environment +} + #[tokio::main] pub async fn main() { - let app = Router::<()>::new().route("/greet", get(greet_handler)); // $ Source=greet + let app = Router::<()>::new() + .route("/greet", get(greet_handler)) // $ Source=greet + .route("/request-host", get(request_host_handler)) // $ Source=requestHost + .route("/config-host", get(config_host_handler)) + .route("/environment", get(environment_handler)); let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") .await .unwrap();