Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
65f7463
Initial plan
Copilot Feb 20, 2026
eb4d30d
Add XXE query for Rust (CWE-611)
Copilot Feb 20, 2026
dce8bcd
Fix: remove xmlCtxtUseOptions from XXE model (not an XML content sink)
Copilot Feb 20, 2026
6f64839
Merge branch 'main' into copilot/add-xxe-query-for-rust
geoffw0 Mar 27, 2026
bd195e5
Rust: Add MaD barriers, since we have that feature now.
geoffw0 Mar 27, 2026
0cb077c
Rust: Add test cases for xmlReadFd, xmlCtxtReadFile that were stubbed…
geoffw0 Feb 24, 2026
9758438
Rust: Add a test case showing the lack of data flow on flag values.
geoffw0 Feb 24, 2026
71942b5
Merge branch 'main' into copilot/add-xxe-query-for-rust
geoffw0 Jun 23, 2026
c3bbe94
Rust: Add libxml dependency.
geoffw0 Jun 23, 2026
2f3bd41
Rust: Replace stub calls with actual calls. Note that we lose result…
geoffw0 Jun 23, 2026
3523670
Rust: Delete the stubs.
geoffw0 Jun 24, 2026
a322744
Rust: Unstub the constants as well.
geoffw0 Jun 24, 2026
ae58964
Rust: Convert sinks to Models As Data, fixing in the process.
geoffw0 Jun 24, 2026
4f4271d
Rust: Add a couple more test cases for literal flag values.
geoffw0 Jun 24, 2026
578c8a2
Rust: Make the integer literal decoding more accurate using an existi…
geoffw0 Jun 24, 2026
ecfd19c
Rust: Clean up the test file.
geoffw0 Jun 24, 2026
8e1a7e1
Rust: Add the new quite to suite lists.
geoffw0 Jun 25, 2026
37c6198
Rust: Add test cases for heuristic sink matching.
geoffw0 Jun 25, 2026
125fe7b
Rust: Add heuristic sinks.
geoffw0 Jun 25, 2026
a820776
Rust: Exclude duplicate results.
geoffw0 Jun 25, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
ql/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-327/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-611/Xxe.ql
ql/rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
ql/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-327/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-611/Xxe.ql
ql/rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
ql/rust/ql/src/queries/security/CWE-696/BadCtorInitialization.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ ql/rust/ql/src/queries/security/CWE-312/CleartextStorageDatabase.ql
ql/rust/ql/src/queries/security/CWE-319/UseOfHttp.ql
ql/rust/ql/src/queries/security/CWE-327/BrokenCryptoAlgorithm.ql
ql/rust/ql/src/queries/security/CWE-327/WeakSensitiveDataHashing.ql
ql/rust/ql/src/queries/security/CWE-611/Xxe.ql
ql/rust/ql/src/queries/security/CWE-614/InsecureCookie.ql
ql/rust/ql/src/queries/security/CWE-770/UncontrolledAllocationSize.ql
ql/rust/ql/src/queries/security/CWE-798/HardcodedCryptographicValue.ql
Expand Down
16 changes: 16 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/libxml.model.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
extensions:
- addsTo:
pack: codeql/rust-all
extensible: sinkModel
data:
- ["libxml::bindings::xmlReadFile", "Argument[0].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlCtxtReadFile", "Argument[1].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlReadDoc", "Argument[0].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlCtxtReadDoc", "Argument[1].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlReadFd", "Argument[0]", "xxe", "manual"]
- ["libxml::bindings::xmlCtxtReadFd", "Argument[1]", "xxe", "manual"]
- ["libxml::bindings::xmlReadMemory", "Argument[0].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlCtxtReadMemory", "Argument[1].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlReadIO", "Argument[0].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlCtxtReadIO", "Argument[1].Reference", "xxe", "manual"]
- ["libxml::bindings::xmlParseInNodeContext", "Argument[1].Reference", "xxe", "manual"]
2 changes: 2 additions & 0 deletions rust/ql/lib/codeql/rust/frameworks/stdlib/core.model.yml
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,8 @@ extensions:
# Str
- ["<core::str>::as_str", "Argument[self].Reference", "ReturnValue.Reference", "taint", "manual"]
- ["<core::str>::as_bytes", "Argument[self].Reference", "ReturnValue.Reference", "taint", "manual"]
- ["<core::str>::as_ptr", "Argument[self].Reference", "ReturnValue.Reference", "taint", "manual"]
- ["<core::str>::as_mut_ptr", "Argument[self].Reference", "ReturnValue.Reference", "taint", "manual"]
- ["<core::str>::parse", "Argument[self].Reference", "ReturnValue.Field[core::result::Result::Ok(0)]", "taint", "manual"]
- ["<core::str>::trim", "Argument[self].Reference", "ReturnValue.Reference", "taint", "manual"]
- ["<core::str>::to_string", "Argument[self].Reference", "ReturnValue", "taint", "manual"]
Expand Down
110 changes: 110 additions & 0 deletions rust/ql/lib/codeql/rust/security/XxeExtensions.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
/**
* 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.FlowBarrier
private import codeql.rust.dataflow.FlowSink
private import codeql.rust.Concepts
private import codeql.rust.dataflow.internal.Node as Node

/**
* 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 sink for XXE from model data.
*/
private class ModelsAsDataSink extends Sink {
ModelsAsDataSink() {
exists(Call call |
// an XML parse call
sinkNode(this, "xxe") and
call = this.(Node::FlowSummaryNode).getSinkElement().getCall() and
// with an unsafe option
hasXxeOption(call.getAnArgument(), _)
)
}
}

/**
* A heuristic sink for XXE.
*/
private class HeuristicSink extends Sink {
HeuristicSink() {
exists(Call call |
// a call that looks it might do XML parsing (this is broad)
call.getStaticTarget().getName().getText().regexpMatch("(?i).*(xml|parse).*") and
// with an unsafe option; we require the option to be named (e.g. `XML_PARSE_NOENT`), not a literal value
// (e.g. `2`), to provide additional confidence that we're actually looking at XML parsing)
hasXxeOption(call.getAnArgument(), true) and
// the sink is any input argument
this.asExpr() = call.getAnArgument() and
// don't duplicate modeled sinks
not exists(ModelsAsDataSink s | s.(Node::FlowSummaryNode).getSinkElement().getCall() = call)
)
}
}

/**
* A barrier for XXE vulnerabilities from model data.
*/
private class ModelsAsDataBarrier extends Barrier {
ModelsAsDataBarrier() { barrierNode(this, "xxe") }
}
}

/**
* 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).
*
* `named` is true if the expression is a named constant, false if it is an
* integer literal.
*/
private predicate hasXxeOption(Expr e, boolean named) {
// named constant XML_PARSE_NOENT or XML_PARSE_DTDLOAD (or very similar)
e.(PathExpr).getPath().getText().matches(["%_PARSE_NOENT", "%_PARSE_DTDLOAD"]) and
named = true
or
// integer literal with XML_PARSE_NOENT (bit 1) or XML_PARSE_DTDLOAD (bit 2) set
exists(string value |
e.(IntegerLiteralExpr).getTextValue() = value + concat(e.(IntegerLiteralExpr).getSuffix()) and
value.toInt().bitAnd(6) != 0 // 6 = 2 | 4 = XML_PARSE_NOENT | XML_PARSE_DTDLOAD
) and
named = false
or
// bitwise OR expression
hasXxeOption(e.(BinaryExpr).getLhs(), named)
or
hasXxeOption(e.(BinaryExpr).getRhs(), named)
or
// cast expression (e.g., `XML_PARSE_NOENT as i32`)
hasXxeOption(e.(CastExpr).getExpr(), named)
}
4 changes: 4 additions & 0 deletions rust/ql/src/change-notes/2026-02-20-xxe.md
Original file line number Diff line number Diff line change
@@ -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.
50 changes: 50 additions & 0 deletions rust/ql/src/queries/security/CWE-611/Xxe.qhelp
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<!DOCTYPE qhelp PUBLIC
"-//Semmle//qhelp//EN"
"qhelp.dtd">
<qhelp>

<overview>
<p>
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.
</p>
<p>
The Rust <code>libxml</code> crate (bindings to C's <code>libxml2</code>
library) exposes several XML parsing functions that accept a parser options
argument. The options <code>XML_PARSE_NOENT</code> and
<code>XML_PARSE_DTDLOAD</code> enable external entity expansion and loading of
external DTD subsets, respectively. Enabling these options when parsing
user-controlled XML is dangerous.
</p>
</overview>

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

<example>
<p>
In the following example, the program reads an XML document supplied by the
user and parses it with external entity expansion enabled:
</p>
<sample src="examples/XxeBad.rs"/>
<p>
The following example shows a corrected version that parses with safe options:
</p>
<sample src="examples/XxeGood.rs"/>
</example>

<references>
<li>OWASP: <a href="https://owasp.org/www-community/vulnerabilities/XML_External_Entity_(XXE)_Processing">XML External Entity (XXE) Processing</a>.</li>
<li>CWE: <a href="https://cwe.mitre.org/data/definitions/611.html">CWE-611: Improper Restriction of XML External Entity Reference</a>.</li>
</references>

</qhelp>
52 changes: 52 additions & 0 deletions rust/ql/src/queries/security/CWE-611/Xxe.ql
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/**
* @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 isAdditionalFlowStep(DataFlow::Node pred, DataFlow::Node succ) {
// we need flow through casts as a *value* step, not just the default taint step,
// to get flow on reference content when the pointer itself is cast.
pred.asExpr() = succ.asExpr().(CastExpr).getExpr()
}

predicate observeDiffInformedIncrementalMode() { any() }
}

module XxeFlow = TaintTracking::Global<XxeConfig>;

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"
16 changes: 16 additions & 0 deletions rust/ql/src/queries/security/CWE-611/examples/XxeBad.rs
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
16 changes: 16 additions & 0 deletions rust/ql/src/queries/security/CWE-611/examples/XxeGood.rs
Original file line number Diff line number Diff line change
@@ -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,
);
}
}
Loading
Loading