What XXE attacks exploit

An XML document can define entities in its DOCTYPE declaration. Some parsers may resolve those entities from local files, network locations, or recursive references. If an attacker controls the XML input, they may use this to:

  • read local files such as /etc/passwd
  • trigger outbound requests to internal services
  • cause denial of service through entity expansion
  • leak metadata from cloud environments

A dangerous payload often looks like this:

<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>

If the parser expands &xxe;, the application may accidentally expose file contents in logs, responses, or downstream processing.

Why Rust developers still need to care

Rust prevents memory corruption, but XXE is a logic and configuration problem. A safe Rust program can still be vulnerable if it uses an XML library with insecure defaults or enables features that resolve external entities.

The main risk areas are:

  • parsing XML from users, partners, or uploaded files
  • accepting XML in web APIs, message queues, or batch jobs
  • converting XML into internal data structures without validation
  • using XML libraries that support DTDs, entity resolution, or external fetches

The safest approach is to treat all XML as untrusted and explicitly disable dangerous features.

Choose a parser with secure defaults

Different Rust XML crates have different security profiles. Before adopting one, check whether it:

  • disables external entity resolution by default
  • avoids DTD processing unless explicitly enabled
  • does not fetch remote resources
  • supports streaming parsing for large inputs

The table below summarizes common design choices you should look for.

Security propertySafer choiceRiskier choice
DTD supportDisabled by defaultEnabled automatically
External entitiesNot resolvedResolved from file or network
Network accessNever during parsingParser may fetch URLs
Memory usageStreaming or boundedFull document expansion
Error handlingReject malformed inputRecover and continue silently

If a crate exposes parser configuration, make the secure configuration part of your application startup, not an optional code path.

A defensive parsing strategy

A good XXE defense in Rust has three layers:

  1. Reject features you do not need
  • disable DTDs
  • disable external entities
  • disable network access
  1. Limit input size
  • cap request body size
  • cap file size before parsing
  1. Validate the resulting data
  • check required fields
  • enforce schema-like constraints in application code

Even if your parser does not support external entity resolution, size limits still matter. XML can be abused with deeply nested structures or large text nodes to consume CPU and memory.

Example: secure XML parsing with quick-xml

quick-xml is commonly used for streaming XML parsing in Rust. A streaming parser is useful because it processes input incrementally instead of loading and expanding the entire document at once.

The example below shows a simple pattern for parsing a small XML document while enforcing a size limit and extracting only the fields you expect.

use quick_xml::events::Event;
use quick_xml::Reader;
use std::io::Cursor;

#[derive(Debug)]
struct UserRecord {
    id: String,
    email: String,
}

fn parse_user_record(xml: &[u8]) -> Result<UserRecord, String> {
    const MAX_XML_SIZE: usize = 16 * 1024;

    if xml.len() > MAX_XML_SIZE {
        return Err("XML input too large".into());
    }

    let mut reader = Reader::from_reader(Cursor::new(xml));
    reader.config_mut().trim_text(true);

    let mut buf = Vec::new();
    let mut id = None;
    let mut email = None;
    let mut current_tag = None;

    loop {
        match reader.read_event_into(&mut buf) {
            Ok(Event::Start(e)) => {
                current_tag = Some(String::from_utf8_lossy(e.name().as_ref()).to_string());
            }
            Ok(Event::Text(e)) => {
                let text = e.unescape().map_err(|_| "Invalid XML text")?.into_owned();
                match current_tag.as_deref() {
                    Some("id") => id = Some(text),
                    Some("email") => email = Some(text),
                    _ => {}
                }
            }
            Ok(Event::End(_)) => {
                current_tag = None;
            }
            Ok(Event::Eof) => break,
            Err(_) => return Err("Malformed XML".into()),
            _ => {}
        }
        buf.clear();
    }

    let id = id.ok_or("Missing id")?;
    let email = email.ok_or("Missing email")?;

    if !email.contains('@') {
        return Err("Invalid email".into());
    }

    Ok(UserRecord { id, email })
}

This example does not attempt to interpret DTDs or external entities. It also rejects oversized input before parsing and validates the fields after extraction.

Avoid parser features that expand untrusted content

Some XML libraries support advanced features for compatibility with older documents. Those features are often unnecessary in modern APIs and can create security exposure.

Be cautious with:

  • DOCTYPE declarations
  • entity expansion
  • XInclude processing
  • schema fetching over the network
  • automatic resolution of external references

If your application must accept XML from third parties, prefer a minimal subset of XML. In many cases, the safest policy is to reject any document containing a DOCTYPE declaration at all.

Practical policy

For untrusted XML, enforce a rule like:

  • allow elements and attributes
  • reject DTDs
  • reject external references
  • reject documents over a fixed size
  • reject unknown fields if your protocol is strict

This reduces the parser’s attack surface and makes the application easier to reason about.

Detect and reject suspicious XML early

It is often useful to perform a lightweight pre-check before full parsing. This is not a replacement for secure parsing, but it can help enforce policy consistently.

For example, you can reject documents containing <!DOCTYPE before they reach the parser:

fn reject_dangerous_xml(xml: &str) -> Result<(), &'static str> {
    if xml.contains("<!DOCTYPE") {
        return Err("DOCTYPE declarations are not allowed");
    }
    Ok(())
}

This check is simple and effective for many internal APIs. However, do not rely on string matching alone if attackers can use unusual whitespace, encoding tricks, or alternate transport layers. Use it as a defense-in-depth measure, not the only control.

Handle XML in web services carefully

When XML arrives through HTTP, the parser is only one part of the security story. You should also:

  • cap request body size at the web framework level
  • set request timeouts
  • avoid logging raw XML bodies
  • return generic parse errors to clients
  • isolate XML processing from sensitive internal networks

A common mistake is to parse XML and then include the original payload in an error response or debug log. If the payload contains secrets, those secrets may be exposed even if the parser itself is safe.

Recommended service pattern

  1. Read at most a fixed number of bytes.
  2. Reject content types you do not support.
  3. Scan for forbidden constructs like DOCTYPE.
  4. Parse with a streaming parser.
  5. Validate the extracted fields.
  6. Never echo the raw XML back to the client.

Compare secure and insecure approaches

ApproachBenefitSecurity concern
Full DOM parsingEasy random accessHigher memory use, possible entity expansion
Streaming parsingLower memory footprintRequires more careful state handling
Allowing DTDsBetter compatibilityEnables XXE if entities are resolved
Rejecting DTDsStronger security postureMay break legacy documents
Size-limited inputPrevents resource abuseMust be enforced consistently

For untrusted input, streaming parsing plus strict rejection rules is usually the best tradeoff.

Test your defenses

Security controls should be tested like any other behavior. Add unit and integration tests for malicious XML samples, not just valid documents.

Useful test cases include:

  • a document with DOCTYPE
  • a document with an external entity reference
  • a very large document
  • deeply nested elements
  • malformed XML with broken tags
  • valid XML with unexpected fields

Example test:

#[test]
fn rejects_doctype() {
    let xml = r#"<?xml version="1.0"?>
<!DOCTYPE data [
  <!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<data>&xxe;</data>"#;

    assert!(reject_dangerous_xml(xml).is_err());
}

These tests help ensure that future refactors do not reintroduce unsafe behavior.

Operational hardening tips

Secure parsing is stronger when combined with runtime controls:

  • run the service with minimal filesystem permissions
  • block outbound network access from XML-processing components
  • isolate parsing in a separate worker process if documents are highly untrusted
  • monitor for parse failures and unusual input sizes
  • keep XML dependencies up to date

Network egress restrictions are especially valuable. Even if a parser were misconfigured, it would be unable to reach internal metadata services or remote attacker infrastructure.

When XML is the wrong choice

Sometimes the best XXE defense is to avoid XML entirely. If you control both ends of an API, consider a simpler format such as JSON, CBOR, or MessagePack. These formats do not have DTDs or external entities, which removes an entire class of parser attacks.

That said, XML is often unavoidable in enterprise systems. In those cases, the goal is not to eliminate XML, but to process it with a narrow, explicit security policy.

Summary

XXE attacks exploit XML features that resolve external or recursive entities. Rust does not prevent these attacks automatically, so secure XML handling depends on parser choice, configuration, input limits, and validation.

The safest pattern is straightforward:

  • use a parser with secure defaults
  • reject DOCTYPE and external entities
  • enforce strict size limits
  • prefer streaming parsing
  • validate extracted data explicitly
  • test malicious inputs as part of your security suite

If your application only needs a small subset of XML, keep the parser’s capabilities as small as possible.

Learn more with useful resources