Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
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
102 changes: 102 additions & 0 deletions rust/ql/lib/codeql/rust/security/XxeExtensions.qll
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
/**
* 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 = "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())
}
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>
46 changes: 46 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,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<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,
);
}
}
7 changes: 7 additions & 0 deletions rust/ql/test/query-tests/security/CWE-611/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

98 changes: 98 additions & 0 deletions rust/ql/test/query-tests/security/CWE-611/Xxe.expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
#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: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: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 | |
| 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 | |
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: <core::option::Option>::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 |
| 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
4 changes: 4 additions & 0 deletions rust/ql/test/query-tests/security/CWE-611/Xxe.qlref
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
query: queries/security/CWE-611/Xxe.ql
postprocess:
- utils/test/PrettyPrintModels.ql
- utils/test/InlineExpectationsTestQuery.ql
Loading