Skip to content

CWE-91: XML Injection (aka Blind XPath Injection)

Overview

XML Injection occurs when untrusted input is concatenated into XML markup, or into an XPath or XQuery expression, instead of being handed to the library as data. The attacker then gets to change what the document contains or what the query matches.

Relationship to Other CWEs

  • CWE-91 (this page) - untrusted data that changes the structure of an XML document being built, or the logic of an XPath or XQuery expression being evaluated
  • CWE-74 (Injection) - the parent
  • CWE-643 (XPath Injection) and CWE-652 (XQuery Injection) - the narrower, query-specific children, neither with a page here. MITRE's full name for this page, "XML Injection (aka Blind XPath Injection)", reflects that it covers them. Where a finding places untrusted input directly into an XPath or XQuery expression rather than XML markup, the fix is closer to SQL injection's parameterization pattern than to markup escaping: avoid string-built query expressions and bind the value as an XPath variable instead of concatenating it
  • CWE-611 (Improper Restriction of XML External Entity Reference) - not this page. A finding about DOCTYPE declarations, external entities, entity expansion, or disallow-doctype-decl belongs there, and MITRE's own mapping notes for CWE-91 name that confusion explicitly. Hardening a parser does not address this page, and building documents with a safe API does not address XXE - an application that both emits and consumes XML needs both fixes

OWASP Classification

A05:2025 - Injection

Risk

High: An attacker who controls part of a document can add elements the receiver was never written to handle. One who controls part of a query can rewrite its logic to bypass an authentication check, or read a stored value back a character at a time through the blind variant.

Remediation Steps

Core Principle: Never construct XML by concatenating untrusted input; use XML libraries that treat user input as data and automatically escape it so it cannot alter document structure.

Trace the Data Path

Work out how untrusted data reaches the XML construction or query named in the finding:

  • Source: where untrusted data enters - user input, an external file, a database, a network request
  • Sink: XML document construction or serialization, or an XPath/XQuery expression handed to an evaluator
  • String concatenation: the point between the two where the untrusted value is joined into an XML string or a query expression

Use XML Libraries with Built-in Escaping (Primary Defense)

Replace string concatenation with an XML API that treats the value as data:

  • Build the document with a DOM builder or streaming writer such as ElementTree, DocumentBuilder, XmlWriter or xmlbuilder2
  • Let the library do the escaping, so < is written as &lt; and > as &gt;
  • Do not join strings to build XML, and do not escape by hand

Validate and Sanitize Input (Defense in Depth)

Validation is a second layer, not the fix:

  • Check type, length and format before the value reaches the XML code, and use an allowlist for enumerated values
  • Reject values carrying XML metacharacters (<, >, &, ', ") where the field's format does not permit them - do not escape here, that belongs at the sink and doing it twice produces &amp;lt; in the output
  • A free-text field such as a bio or a comment legitimately contains & and <, so validation cannot be the control that protects it
  • Length limits also reduce denial-of-service exposure

Bind Variables in XPath and XQuery Expressions (Primary Defense for Query Findings)

A finding that concatenates into a query expression needs a different fix from one that concatenates into markup, and escaping XML entities does nothing for it:

  • Move the value out of the expression and reference it as a variable ($name), the same way a SQL driver binds a parameter
  • Every major ecosystem has this: lxml's tree.xpath("//user[name=$n]", n=value), Java's XPath.setXPathVariableResolver(), .NET's XPathExpression.SetContext() with a custom XsltContext, and the xpath npm package's xpath.parse(expr).select({node, variables})
  • Where the API in use genuinely has no binding (xml.etree.ElementTree, which supports only a restricted XPath subset), evaluate a static expression that selects the candidate node set and compare the value in application code
  • Do not try to escape the value and put it back in the expression - see the pitfall below for why there is nothing to escape it to

Monitor and Test

  • Test with XML injection payloads: </name><admin>true</admin><name>, <![CDATA[<script>alert(1)</script>]]>
  • Test with encoded characters: &lt;script&gt;, &#60;admin&#62;
  • Test XPath sinks with ' or '1'='1 and confirm the query returns only the matching record, not every record
  • Log XML parsing errors and alert on malformed or unexpected XML input
  • Replay the input from the original finding and confirm it no longer changes the document or the query result
  • Check that legitimate values still work: a bound query and an application-side comparison must both find the record for a value containing a quote or an ampersand
  • Re-scan with the security scanner to confirm the issue is resolved

Common Vulnerable Patterns

  • Concatenating untrusted data into XML documents
  • Concatenating untrusted data into an XPath or XQuery expression
  • Escaping XML by hand instead of letting a serializer escape the text it writes

String Concatenation for XML Construction (Pseudocode)

# Dangerous: user input in XML
xml = f"<user><name>{user_input}</name></user>"

Why this is vulnerable: A value carrying </name> closes the element early and everything after it is parsed as markup, so the attacker adds siblings the document was never meant to contain - </name><role>admin</role> is the shape that matters when the receiver reads a field the sender was not supposed to control. What the receiver then does with a duplicate element is not predictable from the payload: ElementTree's find(), XmlNode.SelectSingleNode and .NET's XmlSerializer all take the first occurrence, while xml2js and fast-xml-parser turn it into an array. So the attacker's leverage is that they get to choose the shape the receiver was never written to handle, not that a later element reliably overrides an earlier one.

Even without that, the encoding is wrong: an unescaped & or < makes the document not well-formed, so the parse fails and a routine input becomes an outage. Build the document with a DOM or a serializer that escapes text nodes, rather than escaping by hand - the correct set differs between text content, attribute values and CDATA.

String Concatenation into an XPath Expression (Pseudocode)

# Dangerous: user input in an XPath query
nodes = doc.xpath("//user[name='" + user_input + "' and password='" + secret + "']")

Why this is vulnerable: The attacker is not adding elements, they are rewriting the boolean logic of the query. Derive what the template produces before quoting a payload, because and binds tighter than or in XPath and the precedence decides whether the classic one fires. Measured against a two-user document with this two-clause filter: ' or '1'='1 in the name field alone yields name='' or ('1'='1' and password='x'), which matches nothing. The same value in both fields, or ' or 1=1 or '1'='1 in the name field, returns every user. Against the single-clause form //user[name='X'] the bare ' or '1'='1 does work, which is why the same payload behaves differently on the language pages.

The blind variant that gives CWE-91 its "Blind XPath Injection" alias needs no output at all - only a yes/no signal such as whether the login succeeded. Sending ' or substring(//user[1]/password,1,1)='s' or '1'='2 as the name returned two users when the guess was right and none when it was wrong, which reads out a stored secret one character at a time from an endpoint that never prints it.

Escaping XML entities is not a fix here, because the value is not landing in markup. Neither is escaping the quote: XPath 1.0 string literals have no escape sequence for their own delimiter, so '' and \' are both parse errors rather than a literal quote.

Secure Patterns

XML Library with Automatic Escaping (Python)

# Safe: use XML library to build document
import xml.etree.ElementTree as ET
user_elem = ET.Element('user')
name_elem = ET.SubElement(user_elem, 'name')
name_elem.text = user_input  # Library escapes content
xml = ET.tostring(user_elem)

Why this works:

  • The library escapes <, >, &, " and ' when it serializes the text node, so the value cannot close the element or open a new one
  • The input only ever becomes text content; it cannot become an element, an attribute or a CDATA section, and no string is joined by hand for it to break out of

XPath Variable Binding (Pseudocode)

# Safe: the value is bound, not concatenated
nodes = doc.xpath("//user[name=$n]", n=user_input)

Why this works:

  • The expression compiled by the evaluator is a fixed string with no attacker-controlled characters in it
  • $n resolves to a single string value at evaluation time, so quotes, or and function calls inside it are compared as text rather than parsed as syntax
  • It accepts values that quote-stripping or escaping would break - O'Brien matches, where the concatenated form cannot express it at all
  • The same binding covers XQuery, where the equivalent is a declared external variable rather than an interpolated string

Common Pitfalls

  • Escaping text content but not element or attribute names: Switching from string concatenation to a builder API but still constructing an element or attribute name from untrusted input (e.g., builder.ele(user_input)) - many builder APIs only auto-escape values passed to a dedicated text/attribute-value method, not names, so this reintroduces structural injection through a different call.
  • Assuming standard XML escaping makes a value CDATA-safe: Placing an "escaped" value inside a <![CDATA[...]]> section - the five standard XML entity escapes (<, >, &, ', ") do not cover the literal sequence ]]>, which still terminates the CDATA section early regardless of how the surrounding text was escaped.
  • Escaping the value and putting it back into the XPath expression: There is nothing to escape it to. XPath 1.0 string literals cannot contain their own delimiter and define no escape sequence for it, so 'O''Brien' and 'O\'Brien' are both parse errors rather than a literal quote - verified on lxml 6.1. Switching the delimiter to " only moves the problem to values containing a double quote, and stripping quotes instead silently corrupts legitimate data. Bind a variable, or select a static node set and compare in application code.
  • Hardening the parser and calling the injection fixed: Disabling DTD processing and external entities is the fix for CWE-611 and changes nothing about a document built by concatenation or a query built by concatenation. The two findings often appear in the same file, and closing one is easy to mistake for closing both.

Language-Specific Guidance

Concrete APIs and framework patterns for each stack:

  • Python - ElementTree, lxml, defusedxml, Django, Flask
  • Java - DOM, StAX, JAXB, Jackson XML, Apache Commons Text
  • JavaScript/Node.js - xmlbuilder2, xml2js, he, Next.js
  • C# - LINQ to XML, XmlWriter, XmlSerializer, ASP.NET Core

Additional Resources