Description
XPath injection occurs when untrusted strings are interpreted as query syntax rather than data. Quotes, logical or path operators and functions can alter predicates or select nodes the application did not intend to expose.
XPathExpression.Compile parses an expression in advance; it does not establish trust. Compiling and reusing an expression built from user input remains unsafe. XmlNamespaceManager resolves namespace prefixes but does not bind query variables.
This differs from XML injection, which changes markup structure, and XXE, which uses external entities to access files or network resources.
Potential impact
- Authentication or authorization conditions may be bypassed.
- XML nodes and attributes outside the permitted scope may be disclosed.
- Errors and repeated queries can reveal the XML structure and its data.
- Complex attacker-controlled expressions can increase CPU use or response time.
Remediation
Keep XPath structure under developer control and separate untrusted values from syntax.
- Bind arbitrary strings as variables in a fixed expression. In .NET, implement a custom
XsltContextandIXsltContextVariable, then assign the context withXPathExpression.SetContext. - For a constrained domain such as numbers, parse the format and range strictly, then use the normalized typed value in the appropriate XPath position. Numeric validation is not a replacement for handling string positions.
- If users select among queries, map a token to developer-authored fixed XPath. Do not pass the token itself as an expression.
- If complete expressions must be accepted, require an exact match against an immutable local set of literals. The application must control the contents of that list and who can change it.
XML/HTML encoders, SecurityElement.Escape, quote replacement, XmlNamespaceManager and arbitrary helpers named Sanitize do not provide general XPath parameterization. String literals, numbers, names and paths have different syntax; avoid assembling arbitrary expressions.
Examples
Before
using Microsoft.AspNetCore.Mvc;
using System.Xml;
XmlNode? FindUser(XmlDocument document, [FromQuery] string name)
{
var expression = "//users/user[@name='" + name + "']";
return document.SelectSingleNode(expression);
}
A name such as ' or '1'='1 changes the predicate's structure.
After
Bind string values as variables
A custom XsltContext resolves the fixed $name variable. Input is not passed to XPathExpression.Compile.
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.Xml;
using System.Xml.XPath;
using System.Xml.Xsl;
sealed class VariableContext : XsltContext
{
private readonly IReadOnlyDictionary<string, string> values;
public VariableContext(
NameTable nameTable,
IReadOnlyDictionary<string, string> values) : base(nameTable)
{
this.values = values;
}
public override bool Whitespace => true;
public override int CompareDocument(string baseUri, string nextBaseUri) =>
StringComparer.Ordinal.Compare(baseUri, nextBaseUri);
public override bool PreserveWhitespace(XPathNavigator node) => true;
public override IXsltContextFunction ResolveFunction(
string prefix,
string name,
XPathResultType[] argumentTypes) =>
throw new XPathException($"Unknown function: {prefix}:{name}");
public override IXsltContextVariable ResolveVariable(
string prefix,
string name)
{
if (prefix.Length == 0 && values.TryGetValue(name, out var value))
{
return new BoundString(value);
}
throw new XPathException($"Unknown variable: {prefix}:{name}");
}
}
sealed class BoundString : IXsltContextVariable
{
private readonly string value;
public BoundString(string value) => this.value = value;
public bool IsLocal => false;
public bool IsParam => true;
public XPathResultType VariableType => XPathResultType.String;
public object Evaluate(XsltContext context) => value;
}
sealed class Search
{
public XPathNavigator? FindUser(XPathNavigator navigator, [FromQuery] string name)
{
var expression = XPathExpression.Compile("//users/user[@name=$name]");
var values = new Dictionary<string, string> { ["name"] = name };
expression.SetContext(new VariableContext(new NameTable(), values));
return navigator.SelectSingleNode(expression);
}
}
Allow only required variable names and types, and reject unknown variables and functions. Add fixed namespace-prefix mappings to the same context when needed.
Parse numeric domains strictly
using Microsoft.AspNetCore.Mvc;
using System.Globalization;
using System.Xml;
XmlNode? FindUser(XmlDocument document, [FromQuery] string id)
{
if (!int.TryParse(
id,
NumberStyles.None,
CultureInfo.InvariantCulture,
out var numericId))
{
return null;
}
return document.SelectSingleNode($"//users/user[@id={numericId}]");
}
This accepts decimal integers without a sign or whitespace and uses the normalized int in an unquoted numeric position. Check the business-specific range separately.
Map selection tokens to fixed XPath
using Microsoft.AspNetCore.Mvc;
using System.Xml;
XmlNode? FindView(XmlDocument document, [FromQuery] string view)
{
string? expression = view switch
{
"active" => "//users/user[@active='true']",
"admin" => "//users/user[@role='admin']",
_ => null
};
return expression is null
? null
: document.SelectSingleNode(expression);
}
Protecting XPath syntax does not replace authentication and authorization for the results, XXE prevention or protection of XML structure.
Related standards
- CWE: CWE-643: Improper Neutralization of Data within XPath Expressions, CWE 4.20
- OWASP Top 10: A05:2025 Injection, A03:2021 Injection
- OWASP ASVS: v5.0.0-1.2.7
References
- Official .NET and .NET Core support policy
- Microsoft Learn: CA3008 — Review code for XPath injection vulnerabilities
- Microsoft Learn: XmlNode.SelectNodes
- Microsoft Learn: XPathNavigator
- Microsoft Learn: XPathExpression.Compile
- Microsoft Learn: XPathExpression.SetContext
- Microsoft Learn: User-defined XPath functions and variables
- Microsoft Learn: LINQ to XML XPath Extensions
- CodeQL: C# XPath injection
- Software Security Weakness Assessment Guide 2021
- Vulnerability analysis and assessment criteria for critical information infrastructure