From 65f74635caaddbe921c1a939d16005b4fb20571e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:58:21 +0000 Subject: [PATCH 1/3] Initial plan From eb4d30de7f20d2f0279381f8877b64b80261b918 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:22:05 +0000 Subject: [PATCH 2/3] Add XXE query for Rust (CWE-611) Co-authored-by: geoffw0 <40627776+geoffw0@users.noreply.github.com> --- .../codeql/rust/security/XxeExtensions.qll | 104 +++++++++++++ rust/ql/src/change-notes/2026-02-20-xxe.md | 4 + .../ql/src/queries/security/CWE-611/Xxe.qhelp | 50 ++++++ rust/ql/src/queries/security/CWE-611/Xxe.ql | 46 ++++++ .../security/CWE-611/examples/XxeBad.rs | 16 ++ .../security/CWE-611/examples/XxeGood.rs | 16 ++ .../query-tests/security/CWE-611/Cargo.lock | 7 + .../query-tests/security/CWE-611/Xxe.expected | 94 ++++++++++++ .../query-tests/security/CWE-611/Xxe.qlref | 3 + .../test/query-tests/security/CWE-611/main.rs | 145 ++++++++++++++++++ .../query-tests/security/CWE-611/options.yml | 1 + 11 files changed, 486 insertions(+) create mode 100644 rust/ql/lib/codeql/rust/security/XxeExtensions.qll create mode 100644 rust/ql/src/change-notes/2026-02-20-xxe.md create mode 100644 rust/ql/src/queries/security/CWE-611/Xxe.qhelp create mode 100644 rust/ql/src/queries/security/CWE-611/Xxe.ql create mode 100644 rust/ql/src/queries/security/CWE-611/examples/XxeBad.rs create mode 100644 rust/ql/src/queries/security/CWE-611/examples/XxeGood.rs create mode 100644 rust/ql/test/query-tests/security/CWE-611/Cargo.lock create mode 100644 rust/ql/test/query-tests/security/CWE-611/Xxe.expected create mode 100644 rust/ql/test/query-tests/security/CWE-611/Xxe.qlref create mode 100644 rust/ql/test/query-tests/security/CWE-611/main.rs create mode 100644 rust/ql/test/query-tests/security/CWE-611/options.yml diff --git a/rust/ql/lib/codeql/rust/security/XxeExtensions.qll b/rust/ql/lib/codeql/rust/security/XxeExtensions.qll new file mode 100644 index 000000000000..baeced78c3d3 --- /dev/null +++ b/rust/ql/lib/codeql/rust/security/XxeExtensions.qll @@ -0,0 +1,104 @@ +/** + * Provides classes and predicates to reason about XML external entity (XXE) + * vulnerabilities. + */ + +import rust +private import codeql.rust.dataflow.DataFlow +private import codeql.rust.dataflow.FlowSink +private import codeql.rust.Concepts + +/** + * Provides default sources, sinks and barriers for detecting XML external + * entity (XXE) vulnerabilities, as well as extension points for adding your + * own. + */ +module Xxe { + /** + * A data flow source for XXE vulnerabilities. + */ + abstract class Source extends DataFlow::Node { } + + /** + * A data flow sink for XXE vulnerabilities. + */ + abstract class Sink extends QuerySink::Range { + override string getSinkType() { result = "Xxe" } + } + + /** + * A barrier for XXE vulnerabilities. + */ + abstract class Barrier extends DataFlow::Node { } + + /** + * An active threat-model source, considered as a flow source. + */ + private class ActiveThreatModelSourceAsSource extends Source, ActiveThreatModelSource { } + + /** + * A libxml2 XML parsing call with unsafe parser options, considered as a + * flow sink. + */ + private class Libxml2XxeSink extends Sink { + Libxml2XxeSink() { + exists(Call call, int xmlArg, int optionsArg | + libxml2ParseCall(call, xmlArg, optionsArg) and + this.asExpr() = call.getPositionalArgument(xmlArg) and + hasXxeOption(call.getPositionalArgument(optionsArg)) + ) + } + } +} + +/** + * Holds if `call` is a call to a `libxml2` XML parsing function, where + * `xmlArg` is the index of the XML content argument and `optionsArg` is the + * index of the parser options argument. + */ +private predicate libxml2ParseCall(Call call, int xmlArg, int optionsArg) { + exists(string fname | call.getStaticTarget().getName().getText() = fname | + fname = "xmlCtxtUseOptions" and xmlArg = 0 and optionsArg = 1 + or + fname = "xmlReadFile" and xmlArg = 0 and optionsArg = 2 + or + fname = ["xmlReadDoc", "xmlReadFd"] and xmlArg = 0 and optionsArg = 3 + or + fname = ["xmlCtxtReadFile", "xmlParseInNodeContext"] and xmlArg = 1 and optionsArg = 3 + or + fname = ["xmlCtxtReadDoc", "xmlCtxtReadFd"] and xmlArg = 1 and optionsArg = 4 + or + fname = "xmlReadMemory" and xmlArg = 0 and optionsArg = 4 + or + fname = "xmlCtxtReadMemory" and xmlArg = 1 and optionsArg = 5 + or + fname = "xmlReadIO" and xmlArg = 0 and optionsArg = 5 + or + fname = "xmlCtxtReadIO" and xmlArg = 1 and optionsArg = 6 + ) +} + +/** + * Holds if `e` is an expression that includes an unsafe `xmlParserOption`, + * specifically `XML_PARSE_NOENT` (value 2, enables entity substitution) or + * `XML_PARSE_DTDLOAD` (value 4, loads external DTD subsets). + */ +private predicate hasXxeOption(Expr e) { + // Named constant XML_PARSE_NOENT or XML_PARSE_DTDLOAD + e.(PathExpr).getPath().getText() = ["XML_PARSE_NOENT", "XML_PARSE_DTDLOAD"] + or + // Integer literal with XML_PARSE_NOENT (bit 1) or XML_PARSE_DTDLOAD (bit 2) set + exists(int v | + v = e.(IntegerLiteralExpr).getTextValue().regexpCapture("^([0-9]+).*$", 1).toInt() + | + v.bitAnd(6) != 0 // 6 = 2 | 4 = XML_PARSE_NOENT | XML_PARSE_DTDLOAD + ) + or + // Bitwise OR expression + hasXxeOption(e.(BinaryExpr).getLhs()) + or + hasXxeOption(e.(BinaryExpr).getRhs()) + or + // Cast expression (e.g., `XML_PARSE_NOENT as i32`) + hasXxeOption(e.(CastExpr).getExpr()) +} diff --git a/rust/ql/src/change-notes/2026-02-20-xxe.md b/rust/ql/src/change-notes/2026-02-20-xxe.md new file mode 100644 index 000000000000..2fc35c11bd57 --- /dev/null +++ b/rust/ql/src/change-notes/2026-02-20-xxe.md @@ -0,0 +1,4 @@ +--- +category: newQuery +--- +* Added a new query, `rust/xxe`, to detect XML external entity (XXE) vulnerabilities in Rust code that uses the `libxml` crate (bindings to C's `libxml2`). The query flags calls to `libxml2` parsing functions with unsafe options (`XML_PARSE_NOENT` or `XML_PARSE_DTDLOAD`) when the XML input comes from a user-controlled source. diff --git a/rust/ql/src/queries/security/CWE-611/Xxe.qhelp b/rust/ql/src/queries/security/CWE-611/Xxe.qhelp new file mode 100644 index 000000000000..753e4fa6c5b1 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-611/Xxe.qhelp @@ -0,0 +1,50 @@ + + + + +

+Parsing XML input with external entity (XXE) expansion enabled while the input +is controlled by a user can lead to a variety of attacks. An attacker who +controls the XML input may be able to use an XML external entity declaration +to read the contents of arbitrary files from the server's file system, perform +server-side request forgery (SSRF), or perform denial-of-service attacks. +

+

+The Rust libxml crate (bindings to C's libxml2 +library) exposes several XML parsing functions that accept a parser options +argument. The options XML_PARSE_NOENT and +XML_PARSE_DTDLOAD enable external entity expansion and loading of +external DTD subsets, respectively. Enabling these options when parsing +user-controlled XML is dangerous. +

+
+ + +

+Do not enable XML_PARSE_NOENT or XML_PARSE_DTDLOAD +when parsing user-controlled XML. Parse XML with safe options (for example, +using 0 as the options argument) to disable external entity +expansion. +

+
+ + +

+In the following example, the program reads an XML document supplied by the +user and parses it with external entity expansion enabled: +

+ +

+The following example shows a corrected version that parses with safe options: +

+ +
+ + +
  • OWASP: XML External Entity (XXE) Processing.
  • +
  • CWE: CWE-611: Improper Restriction of XML External Entity Reference.
  • +
    + +
    diff --git a/rust/ql/src/queries/security/CWE-611/Xxe.ql b/rust/ql/src/queries/security/CWE-611/Xxe.ql new file mode 100644 index 000000000000..cd00f1418168 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-611/Xxe.ql @@ -0,0 +1,46 @@ +/** + * @name XML external entity expansion + * @description Parsing user-controlled XML with external entity expansion + * enabled may lead to disclosure of confidential data or + * server-side request forgery. + * @kind path-problem + * @problem.severity error + * @security-severity 9.1 + * @precision high + * @id rust/xxe + * @tags security + * external/cwe/cwe-611 + * external/cwe/cwe-776 + * external/cwe/cwe-827 + */ + +import rust +import codeql.rust.dataflow.DataFlow +import codeql.rust.dataflow.TaintTracking +import codeql.rust.security.XxeExtensions + +/** + * A taint configuration for user-controlled data reaching an XML parser with + * external entity expansion enabled. + */ +module XxeConfig implements DataFlow::ConfigSig { + import Xxe + + 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 XxeFlow = TaintTracking::Global; + +import XxeFlow::PathGraph + +from XxeFlow::PathNode sourceNode, XxeFlow::PathNode sinkNode +where XxeFlow::flowPath(sourceNode, sinkNode) +select sinkNode.getNode(), sourceNode, sinkNode, + "XML parsing depends on a $@ without guarding against external entity expansion.", + sourceNode.getNode(), "user-provided value" diff --git a/rust/ql/src/queries/security/CWE-611/examples/XxeBad.rs b/rust/ql/src/queries/security/CWE-611/examples/XxeBad.rs new file mode 100644 index 000000000000..d3e8ba137c0c --- /dev/null +++ b/rust/ql/src/queries/security/CWE-611/examples/XxeBad.rs @@ -0,0 +1,16 @@ +use libxml::bindings::{xmlReadMemory, XML_PARSE_NOENT}; +use std::ffi::CString; + +fn parse_user_xml(user_input: &str) { + let c_input = CString::new(user_input).unwrap(); + // BAD: external entity expansion is enabled via XML_PARSE_NOENT + unsafe { + xmlReadMemory( + c_input.as_ptr(), + c_input.as_bytes().len() as i32, + std::ptr::null(), + std::ptr::null(), + XML_PARSE_NOENT as i32, + ); + } +} diff --git a/rust/ql/src/queries/security/CWE-611/examples/XxeGood.rs b/rust/ql/src/queries/security/CWE-611/examples/XxeGood.rs new file mode 100644 index 000000000000..b7c0728020c4 --- /dev/null +++ b/rust/ql/src/queries/security/CWE-611/examples/XxeGood.rs @@ -0,0 +1,16 @@ +use libxml::bindings::xmlReadMemory; +use std::ffi::CString; + +fn parse_user_xml(user_input: &str) { + let c_input = CString::new(user_input).unwrap(); + // GOOD: safe options (0) disable external entity expansion + unsafe { + xmlReadMemory( + c_input.as_ptr(), + c_input.as_bytes().len() as i32, + std::ptr::null(), + std::ptr::null(), + 0, + ); + } +} diff --git a/rust/ql/test/query-tests/security/CWE-611/Cargo.lock b/rust/ql/test/query-tests/security/CWE-611/Cargo.lock new file mode 100644 index 000000000000..b9856cfaf77d --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-611/Cargo.lock @@ -0,0 +1,7 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "test" +version = "0.0.1" diff --git a/rust/ql/test/query-tests/security/CWE-611/Xxe.expected b/rust/ql/test/query-tests/security/CWE-611/Xxe.expected new file mode 100644 index 000000000000..20285b29ff9c --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-611/Xxe.expected @@ -0,0 +1,94 @@ +#select +| main.rs:68:19:68:26 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:68:19:68:26 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +| main.rs:73:19:73:26 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:73:19:73:26 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +| main.rs:78:19:78:26 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:78:19:78:26 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +| main.rs:83:17:83:29 | user_filename | main.rs:133:25:133:38 | ...::args | main.rs:83:17:83:29 | user_filename | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:133:25:133:38 | ...::args | user-provided value | +| main.rs:88:16:88:23 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:88:16:88:23 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +| main.rs:93:42:93:49 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:93:42:93:49 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +| main.rs:100:9:100:16 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:100:9:100:16 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +| main.rs:110:19:110:26 | user_xml | main.rs:132:20:132:33 | ...::args | main.rs:110:19:110:26 | user_xml | XML parsing depends on a $@ without guarding against external entity expansion. | main.rs:132:20:132:33 | ...::args | user-provided value | +edges +| main.rs:66:25:66:38 | ...: ... [&ref] | main.rs:68:19:68:26 | user_xml | provenance | | +| main.rs:71:27:71:40 | ...: ... [&ref] | main.rs:73:19:73:26 | user_xml | provenance | | +| main.rs:76:28:76:41 | ...: ... [&ref] | main.rs:78:19:78:26 | user_xml | provenance | | +| main.rs:81:27:81:45 | ...: ... [&ref] | main.rs:83:17:83:29 | user_filename | provenance | | +| main.rs:86:26:86:39 | ...: ... [&ref] | main.rs:88:16:88:23 | user_xml | provenance | | +| main.rs:91:31:91:44 | ...: ... [&ref] | main.rs:93:42:93:49 | user_xml | provenance | | +| main.rs:96:34:96:47 | ...: ... [&ref] | main.rs:100:9:100:16 | user_xml | provenance | | +| main.rs:108:29:108:42 | ...: ... [&ref] | main.rs:110:19:110:26 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:135:27:135:34 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:136:29:136:36 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:137:30:137:37 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:139:28:139:35 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:140:33:140:40 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:141:36:141:43 | user_xml | provenance | | +| main.rs:132:9:132:16 | user_xml | main.rs:142:31:142:38 | user_xml | provenance | | +| main.rs:132:20:132:33 | ...::args | main.rs:132:20:132:35 | ...::args(...) [element] | provenance | Src:MaD:488 | +| main.rs:132:20:132:35 | ...::args(...) [element] | main.rs:132:20:132:42 | ... .nth(...) [Some] | provenance | MaD:440 | +| main.rs:132:20:132:42 | ... .nth(...) [Some] | main.rs:132:20:132:62 | ... .unwrap_or_default() | provenance | MaD:9229 | +| main.rs:132:20:132:62 | ... .unwrap_or_default() | main.rs:132:9:132:16 | user_xml | provenance | | +| main.rs:133:9:133:21 | user_filename | main.rs:138:29:138:41 | user_filename | provenance | | +| main.rs:133:25:133:38 | ...::args | main.rs:133:25:133:40 | ...::args(...) [element] | provenance | Src:MaD:488 | +| main.rs:133:25:133:40 | ...::args(...) [element] | main.rs:133:25:133:47 | ... .nth(...) [Some] | provenance | MaD:440 | +| main.rs:133:25:133:47 | ... .nth(...) [Some] | main.rs:133:25:133:67 | ... .unwrap_or_default() | provenance | MaD:9229 | +| main.rs:133:25:133:67 | ... .unwrap_or_default() | main.rs:133:9:133:21 | user_filename | provenance | | +| main.rs:135:26:135:34 | &user_xml [&ref] | main.rs:66:25:66:38 | ...: ... [&ref] | provenance | | +| main.rs:135:27:135:34 | user_xml | main.rs:135:26:135:34 | &user_xml [&ref] | provenance | | +| main.rs:136:28:136:36 | &user_xml [&ref] | main.rs:71:27:71:40 | ...: ... [&ref] | provenance | | +| main.rs:136:29:136:36 | user_xml | main.rs:136:28:136:36 | &user_xml [&ref] | provenance | | +| main.rs:137:29:137:37 | &user_xml [&ref] | main.rs:76:28:76:41 | ...: ... [&ref] | provenance | | +| main.rs:137:30:137:37 | user_xml | main.rs:137:29:137:37 | &user_xml [&ref] | provenance | | +| main.rs:138:28:138:41 | &user_filename [&ref] | main.rs:81:27:81:45 | ...: ... [&ref] | provenance | | +| main.rs:138:29:138:41 | user_filename | main.rs:138:28:138:41 | &user_filename [&ref] | provenance | | +| main.rs:139:27:139:35 | &user_xml [&ref] | main.rs:86:26:86:39 | ...: ... [&ref] | provenance | | +| main.rs:139:28:139:35 | user_xml | main.rs:139:27:139:35 | &user_xml [&ref] | provenance | | +| main.rs:140:32:140:40 | &user_xml [&ref] | main.rs:91:31:91:44 | ...: ... [&ref] | provenance | | +| main.rs:140:33:140:40 | user_xml | main.rs:140:32:140:40 | &user_xml [&ref] | provenance | | +| main.rs:141:35:141:43 | &user_xml [&ref] | main.rs:96:34:96:47 | ...: ... [&ref] | provenance | | +| main.rs:141:36:141:43 | user_xml | main.rs:141:35:141:43 | &user_xml [&ref] | provenance | | +| main.rs:142:30:142:38 | &user_xml [&ref] | main.rs:108:29:108:42 | ...: ... [&ref] | provenance | | +| main.rs:142:31:142:38 | user_xml | main.rs:142:30:142:38 | &user_xml [&ref] | provenance | | +nodes +| main.rs:66:25:66:38 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:68:19:68:26 | user_xml | semmle.label | user_xml | +| main.rs:71:27:71:40 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:73:19:73:26 | user_xml | semmle.label | user_xml | +| main.rs:76:28:76:41 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:78:19:78:26 | user_xml | semmle.label | user_xml | +| main.rs:81:27:81:45 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:83:17:83:29 | user_filename | semmle.label | user_filename | +| main.rs:86:26:86:39 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:88:16:88:23 | user_xml | semmle.label | user_xml | +| main.rs:91:31:91:44 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:93:42:93:49 | user_xml | semmle.label | user_xml | +| main.rs:96:34:96:47 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:100:9:100:16 | user_xml | semmle.label | user_xml | +| main.rs:108:29:108:42 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | +| main.rs:110:19:110:26 | user_xml | semmle.label | user_xml | +| main.rs:132:9:132:16 | user_xml | semmle.label | user_xml | +| main.rs:132:20:132:33 | ...::args | semmle.label | ...::args | +| main.rs:132:20:132:35 | ...::args(...) [element] | semmle.label | ...::args(...) [element] | +| main.rs:132:20:132:42 | ... .nth(...) [Some] | semmle.label | ... .nth(...) [Some] | +| main.rs:132:20:132:62 | ... .unwrap_or_default() | semmle.label | ... .unwrap_or_default() | +| main.rs:133:9:133:21 | user_filename | semmle.label | user_filename | +| main.rs:133:25:133:38 | ...::args | semmle.label | ...::args | +| main.rs:133:25:133:40 | ...::args(...) [element] | semmle.label | ...::args(...) [element] | +| main.rs:133:25:133:47 | ... .nth(...) [Some] | semmle.label | ... .nth(...) [Some] | +| main.rs:133:25:133:67 | ... .unwrap_or_default() | semmle.label | ... .unwrap_or_default() | +| main.rs:135:26:135:34 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:135:27:135:34 | user_xml | semmle.label | user_xml | +| main.rs:136:28:136:36 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:136:29:136:36 | user_xml | semmle.label | user_xml | +| main.rs:137:29:137:37 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:137:30:137:37 | user_xml | semmle.label | user_xml | +| main.rs:138:28:138:41 | &user_filename [&ref] | semmle.label | &user_filename [&ref] | +| main.rs:138:29:138:41 | user_filename | semmle.label | user_filename | +| main.rs:139:27:139:35 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:139:28:139:35 | user_xml | semmle.label | user_xml | +| main.rs:140:32:140:40 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:140:33:140:40 | user_xml | semmle.label | user_xml | +| main.rs:141:35:141:43 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:141:36:141:43 | user_xml | semmle.label | user_xml | +| main.rs:142:30:142:38 | &user_xml [&ref] | semmle.label | &user_xml [&ref] | +| main.rs:142:31:142:38 | user_xml | semmle.label | user_xml | +subpaths diff --git a/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref b/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref new file mode 100644 index 000000000000..1098d2d2737c --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref @@ -0,0 +1,3 @@ +query: queries/security/CWE-611/Xxe.ql +postprocess: + - utils/test/InlineExpectationsTestQuery.ql diff --git a/rust/ql/test/query-tests/security/CWE-611/main.rs b/rust/ql/test/query-tests/security/CWE-611/main.rs new file mode 100644 index 000000000000..12bb11773be9 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-611/main.rs @@ -0,0 +1,145 @@ +// Stub types and constants to simulate libxml2 bindings +pub struct XmlDoc; +pub struct XmlParserCtxt; + +// xmlParserOption constants +const XML_PARSE_NOENT: i32 = 2; // substitute entities +const XML_PARSE_DTDLOAD: i32 = 4; // load the external subset + +// Stub libxml2 parsing functions (simplified signatures using &str for clarity) +fn xmlReadFile(_url: &str, _encoding: &str, _options: i32) -> *mut XmlDoc { + std::ptr::null_mut() +} + +fn xmlReadMemory(buffer: &str, _size: i32, _url: &str, _encoding: &str, _options: i32) -> *mut XmlDoc { + let _ = buffer; + std::ptr::null_mut() +} + +fn xmlReadDoc(cur: &str, _url: &str, _encoding: &str, _options: i32) -> *mut XmlDoc { + let _ = cur; + std::ptr::null_mut() +} + +fn xmlReadFd(_fd: i32, _url: &str, _encoding: &str, _options: i32) -> *mut XmlDoc { + std::ptr::null_mut() +} + +fn xmlCtxtReadFile( + _ctxt: *mut XmlParserCtxt, + _url: &str, + _encoding: &str, + _options: i32, +) -> *mut XmlDoc { + std::ptr::null_mut() +} + +fn xmlCtxtReadDoc( + _ctxt: *mut XmlParserCtxt, + cur: &str, + _url: &str, + _encoding: &str, + _options: i32, +) -> *mut XmlDoc { + let _ = cur; + std::ptr::null_mut() +} + +fn xmlCtxtReadMemory( + _ctxt: *mut XmlParserCtxt, + buffer: &str, + _size: i32, + _url: &str, + _encoding: &str, + _options: i32, +) -> *mut XmlDoc { + let _ = buffer; + std::ptr::null_mut() +} + +fn xmlCtxtUseOptions(_ctxt: *mut XmlParserCtxt, _options: i32) -> i32 { + 0 +} + +// --- BAD: user-controlled XML with unsafe parser options --- + +fn test_xml_parse_noent(user_xml: &str) { + // BAD: XML_PARSE_NOENT enables external entity substitution + xmlReadMemory(user_xml, user_xml.len() as i32, "", "", XML_PARSE_NOENT); // $ Alert[rust/xxe] +} + +fn test_xml_parse_dtdload(user_xml: &str) { + // BAD: XML_PARSE_DTDLOAD enables loading of external DTD subsets + xmlReadMemory(user_xml, user_xml.len() as i32, "", "", XML_PARSE_DTDLOAD); // $ Alert[rust/xxe] +} + +fn test_xml_parse_combined(user_xml: &str) { + // BAD: combining both unsafe options + xmlReadMemory(user_xml, user_xml.len() as i32, "", "", XML_PARSE_NOENT | XML_PARSE_DTDLOAD); // $ Alert[rust/xxe] +} + +fn test_xml_read_file_bad(user_filename: &str) { + // BAD: user-controlled filename with XML_PARSE_NOENT + xmlReadFile(user_filename, "", XML_PARSE_NOENT); // $ Alert[rust/xxe] +} + +fn test_xml_read_doc_bad(user_xml: &str) { + // BAD: user-controlled XML document with XML_PARSE_DTDLOAD + xmlReadDoc(user_xml, "", "", XML_PARSE_DTDLOAD); // $ Alert[rust/xxe] +} + +fn test_xml_ctxt_read_doc_bad(user_xml: &str) { + // BAD: user-controlled XML with unsafe options via ctxt variant + xmlCtxtReadDoc(std::ptr::null_mut(), user_xml, "", "", XML_PARSE_NOENT); // $ Alert[rust/xxe] +} + +fn test_xml_ctxt_read_memory_bad(user_xml: &str) { + // BAD: user-controlled XML with unsafe options via ctxt variant + xmlCtxtReadMemory( + std::ptr::null_mut(), + user_xml, // $ Alert[rust/xxe] + user_xml.len() as i32, + "", + "", + XML_PARSE_NOENT, + ); +} + +fn test_integer_literal_bad(user_xml: &str) { + // BAD: literal value 2 = XML_PARSE_NOENT + xmlReadMemory(user_xml, user_xml.len() as i32, "", "", 2); // $ Alert[rust/xxe] +} + +// --- GOOD: user-controlled XML with safe parser options --- + +fn test_xml_parse_safe_options(user_xml: &str) { + // GOOD: options = 0 means no entity expansion + xmlReadMemory(user_xml, user_xml.len() as i32, "", "", 0); + xmlReadFile(user_xml, "", 0); + xmlReadDoc(user_xml, "", "", 0); +} + +// --- GOOD: hardcoded (non-user-controlled) XML with unsafe parser options --- + +fn test_xml_hardcoded_unsafe() { + let xml = ""; + // GOOD: XML content is not user-controlled + xmlReadMemory(xml, xml.len() as i32, "", "", XML_PARSE_NOENT); + xmlReadFile("trusted/input.xml", "", XML_PARSE_NOENT); +} + +fn main() { + let user_xml = std::env::args().nth(1).unwrap_or_default(); // $ Source + let user_filename = std::env::args().nth(2).unwrap_or_default(); // $ Source + + test_xml_parse_noent(&user_xml); + test_xml_parse_dtdload(&user_xml); + test_xml_parse_combined(&user_xml); + test_xml_read_file_bad(&user_filename); + test_xml_read_doc_bad(&user_xml); + test_xml_ctxt_read_doc_bad(&user_xml); + test_xml_ctxt_read_memory_bad(&user_xml); + test_integer_literal_bad(&user_xml); + test_xml_parse_safe_options(&user_xml); + test_xml_hardcoded_unsafe(); +} diff --git a/rust/ql/test/query-tests/security/CWE-611/options.yml b/rust/ql/test/query-tests/security/CWE-611/options.yml new file mode 100644 index 000000000000..c7a0beabb538 --- /dev/null +++ b/rust/ql/test/query-tests/security/CWE-611/options.yml @@ -0,0 +1 @@ +qltest_cargo_check: true From dce8bcdf2ba23f51845baf55d40be3b611551588 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 20 Feb 2026 18:31:49 +0000 Subject: [PATCH 3/3] Fix: remove xmlCtxtUseOptions from XXE model (not an XML content sink) Co-authored-by: geoffw0 <40627776+geoffw0@users.noreply.github.com> --- .../lib/codeql/rust/security/XxeExtensions.qll | 2 -- .../query-tests/security/CWE-611/Xxe.expected | 16 ++++++++++------ .../test/query-tests/security/CWE-611/Xxe.qlref | 1 + 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/rust/ql/lib/codeql/rust/security/XxeExtensions.qll b/rust/ql/lib/codeql/rust/security/XxeExtensions.qll index baeced78c3d3..53f591113982 100644 --- a/rust/ql/lib/codeql/rust/security/XxeExtensions.qll +++ b/rust/ql/lib/codeql/rust/security/XxeExtensions.qll @@ -58,8 +58,6 @@ module Xxe { */ private predicate libxml2ParseCall(Call call, int xmlArg, int optionsArg) { exists(string fname | call.getStaticTarget().getName().getText() = fname | - fname = "xmlCtxtUseOptions" and xmlArg = 0 and optionsArg = 1 - or fname = "xmlReadFile" and xmlArg = 0 and optionsArg = 2 or fname = ["xmlReadDoc", "xmlReadFd"] and xmlArg = 0 and optionsArg = 3 diff --git a/rust/ql/test/query-tests/security/CWE-611/Xxe.expected b/rust/ql/test/query-tests/security/CWE-611/Xxe.expected index 20285b29ff9c..4fa39224d6c3 100644 --- a/rust/ql/test/query-tests/security/CWE-611/Xxe.expected +++ b/rust/ql/test/query-tests/security/CWE-611/Xxe.expected @@ -23,14 +23,14 @@ edges | main.rs:132:9:132:16 | user_xml | main.rs:140:33:140:40 | user_xml | provenance | | | main.rs:132:9:132:16 | user_xml | main.rs:141:36:141:43 | user_xml | provenance | | | main.rs:132:9:132:16 | user_xml | main.rs:142:31:142:38 | user_xml | provenance | | -| main.rs:132:20:132:33 | ...::args | main.rs:132:20:132:35 | ...::args(...) [element] | provenance | Src:MaD:488 | -| main.rs:132:20:132:35 | ...::args(...) [element] | main.rs:132:20:132:42 | ... .nth(...) [Some] | provenance | MaD:440 | -| main.rs:132:20:132:42 | ... .nth(...) [Some] | main.rs:132:20:132:62 | ... .unwrap_or_default() | provenance | MaD:9229 | +| main.rs:132:20:132:33 | ...::args | main.rs:132:20:132:35 | ...::args(...) [element] | provenance | Src:MaD:1 | +| main.rs:132:20:132:35 | ...::args(...) [element] | main.rs:132:20:132:42 | ... .nth(...) [Some] | provenance | MaD:2 | +| main.rs:132:20:132:42 | ... .nth(...) [Some] | main.rs:132:20:132:62 | ... .unwrap_or_default() | provenance | MaD:3 | | main.rs:132:20:132:62 | ... .unwrap_or_default() | main.rs:132:9:132:16 | user_xml | provenance | | | main.rs:133:9:133:21 | user_filename | main.rs:138:29:138:41 | user_filename | provenance | | -| main.rs:133:25:133:38 | ...::args | main.rs:133:25:133:40 | ...::args(...) [element] | provenance | Src:MaD:488 | -| main.rs:133:25:133:40 | ...::args(...) [element] | main.rs:133:25:133:47 | ... .nth(...) [Some] | provenance | MaD:440 | -| main.rs:133:25:133:47 | ... .nth(...) [Some] | main.rs:133:25:133:67 | ... .unwrap_or_default() | provenance | MaD:9229 | +| main.rs:133:25:133:38 | ...::args | main.rs:133:25:133:40 | ...::args(...) [element] | provenance | Src:MaD:1 | +| main.rs:133:25:133:40 | ...::args(...) [element] | main.rs:133:25:133:47 | ... .nth(...) [Some] | provenance | MaD:2 | +| main.rs:133:25:133:47 | ... .nth(...) [Some] | main.rs:133:25:133:67 | ... .unwrap_or_default() | provenance | MaD:3 | | main.rs:133:25:133:67 | ... .unwrap_or_default() | main.rs:133:9:133:21 | user_filename | provenance | | | main.rs:135:26:135:34 | &user_xml [&ref] | main.rs:66:25:66:38 | ...: ... [&ref] | provenance | | | main.rs:135:27:135:34 | user_xml | main.rs:135:26:135:34 | &user_xml [&ref] | provenance | | @@ -48,6 +48,10 @@ edges | main.rs:141:36:141:43 | user_xml | main.rs:141:35:141:43 | &user_xml [&ref] | provenance | | | main.rs:142:30:142:38 | &user_xml [&ref] | main.rs:108:29:108:42 | ...: ... [&ref] | provenance | | | main.rs:142:31:142:38 | user_xml | main.rs:142:30:142:38 | &user_xml [&ref] | provenance | | +models +| 1 | Source: std::env::args; ReturnValue.Element; commandargs | +| 2 | Summary: <_ as core::iter::traits::iterator::Iterator>::nth; Argument[self].Reference.Element; ReturnValue.Field[core::option::Option::Some(0)]; value | +| 3 | Summary: ::unwrap_or_default; Argument[self].Field[core::option::Option::Some(0)]; ReturnValue; value | nodes | main.rs:66:25:66:38 | ...: ... [&ref] | semmle.label | ...: ... [&ref] | | main.rs:68:19:68:26 | user_xml | semmle.label | user_xml | diff --git a/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref b/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref index 1098d2d2737c..450de0af97ee 100644 --- a/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref +++ b/rust/ql/test/query-tests/security/CWE-611/Xxe.qlref @@ -1,3 +1,4 @@ query: queries/security/CWE-611/Xxe.ql postprocess: + - utils/test/PrettyPrintModels.ql - utils/test/InlineExpectationsTestQuery.ql