Improper Neutralization of Data within XQuery Expressions (XQuery Injection)
Description
XQuery Injection occurs when user-supplied data is incorporated into XQuery expressions without proper sanitization. XQuery is a query language for XML databases similar to SQL. When user input is directly concatenated into XQuery strings, attackers can modify the query logic to bypass authentication, extract unauthorized data, or manipulate XML database contents.
Risk
Authentication bypass when XQuery validates credentials. Unauthorized data extraction from XML databases. XML document enumeration and structure discovery. Modification of stored XML data. Denial of service through complex queries. Information disclosure through error messages. Complete database compromise in severe cases.
Solution
Use parameterized XQuery expressions. Validate and sanitize all user input. Escape special XQuery characters. Implement input whitelisting. Use XQuery libraries that support binding variables. Limit query capabilities for untrusted input. Implement proper error handling to prevent information leakage.
Common Consequences
| Impact | Details |
|---|---|
| Confidentiality | Scope: Data Disclosure Unauthorized access to XML data. |
| Authentication | Scope: Bypass Authentication logic circumvented. |
| Integrity | Scope: Data Manipulation XML documents modified without authorization. |
Example Code + Solution Code
Vulnerable Code
// VULNERABLE: XQuery injection in authentication
public class VulnerableXQueryAuth {
private XQConnection connection;
public boolean authenticate(String username, String password) {
// VULNERABLE: Direct string concatenation
String xquery =
"for $user in doc('users.xml')//user " +
"where $user/username = '" + username + "' " +
"and $user/password = '" + password + "' " +
"return $user";
// Attack: username = "admin' or '1'='1' or 'x'='"
// Attack: password = "' or '1'='1"
// Results in query that always returns a user
try {
XQPreparedExpression expr = connection.prepareExpression(xquery);
XQResultSequence result = expr.executeQuery();
return result.next();
} catch (Exception e) {
return false;
}
}
// VULNERABLE: Data extraction
public List<String> searchUsers(String searchTerm) {
// VULNERABLE: User input in query
String xquery =
"for $user in doc('users.xml')//user " +
"where contains($user/name, '" + searchTerm + "') " +
"return $user/email/text()";
// Attack: searchTerm = "') or true() or contains($user/name, '"
// Returns all emails
return executeQuery(xquery);
}
// VULNERABLE: Document access
public String getDocument(String docName) {
// VULNERABLE: Document name from user
String xquery = "doc('" + docName + "')";
// Attack: docName = "users.xml')//password | doc('public.xml"
// Extracts passwords
return executeQuery(xquery);
}
}
# VULNERABLE: Python XQuery injection
from xmldb import XMLDatabase
class VulnerableXMLDatabase:
def __init__(self):
self.db = XMLDatabase()
def login(self, username, password):
# VULNERABLE: Direct string formatting
xquery = f"""
for $user in doc('users.xml')//user
where $user/username = '{username}'
and $user/password = '{password}'
return $user
"""
# Attack: username = "' or '1'='1"
result = self.db.execute(xquery)
return len(result) > 0
def search_products(self, category, min_price):
# VULNERABLE: Multiple injection points
xquery = f"""
for $p in doc('products.xml')//product
where $p/category = '{category}'
and $p/price >= {min_price}
return $p/name/text()
"""
# Attack: category = "electronics' or '1'='1' or 'x'='"
# Attack: min_price = "0 or true()"
return self.db.execute(xquery)
def get_user_data(self, user_id):
# VULNERABLE: ID injection
xquery = f"""
doc('users.xml')//user[@id='{user_id}']/data
"""
# Attack: user_id = "1' or @id='2' or @id='"
# Returns multiple users' data
return self.db.execute(xquery)
// VULNERABLE: Node.js XQuery injection
const xmldb = require('xmldb');
class VulnerableXQueryService {
constructor() {
this.db = new xmldb.Database();
}
async authenticate(username, password) {
// VULNERABLE: Template literal with user input
const xquery = `
for $user in doc('users.xml')//user
where $user/username = '${username}'
and $user/password = '${password}'
return $user
`;
const result = await this.db.query(xquery);
return result.length > 0;
}
async searchDocuments(keyword) {
// VULNERABLE: User input in contains()
const xquery = `
for $doc in collection('documents')//document
where contains($doc/content, '${keyword}')
return $doc
`;
// Attack: keyword = "') or true() or contains($doc/content, '"
return await this.db.query(xquery);
}
async getConfiguration(configName) {
// VULNERABLE: Configuration name injection
const xquery = `doc('config.xml')//config[@name='${configName}']`;
// Attack: configName = "debug'] | //user/password | //config[@name='x"
return await this.db.query(xquery);
}
}
(: VULNERABLE: XQuery within XQuery :)
(: This XQuery module is vulnerable when parameters come from users :)
declare function local:authenticate($username as xs:string, $password as xs:string) {
(: VULNERABLE: Direct use of parameters in query :)
let $query := concat(
"for $user in doc('users.xml')//user ",
"where $user/username = '", $username, "' ",
"and $user/password = '", $password, "' ",
"return $user"
)
return xquery:eval($query)
};
declare function local:search($term as xs:string) {
(: VULNERABLE: User input in FLWOR :)
for $item in doc('items.xml')//item
where contains($item/name, $term) (: Direct use of $term is vulnerable :)
return $item
};
Fixed Code
// SAFE: XQuery with parameterized expressions
public class SafeXQueryAuth {
private XQConnection connection;
public boolean authenticate(String username, String password) {
// SAFE: Use external variables (parameterized query)
String xquery =
"declare variable $user external; " +
"declare variable $pass external; " +
"for $u in doc('users.xml')//user " +
"where $u/username = $user " +
"and $u/password = $pass " +
"return $u";
try {
XQPreparedExpression expr = connection.prepareExpression(xquery);
// Bind parameters safely
expr.bindString(new QName("user"), username, null);
expr.bindString(new QName("pass"), password, null);
XQResultSequence result = expr.executeQuery();
return result.next();
} catch (Exception e) {
return false;
}
}
// SAFE: Validated input
public List<String> searchUsers(String searchTerm) {
// Validate input
if (!isValidSearchTerm(searchTerm)) {
throw new IllegalArgumentException("Invalid search term");
}
String xquery =
"declare variable $term external; " +
"for $user in doc('users.xml')//user " +
"where contains($user/name, $term) " +
"return $user/email/text()";
XQPreparedExpression expr = connection.prepareExpression(xquery);
expr.bindString(new QName("term"), searchTerm, null);
return executeQuery(expr);
}
private boolean isValidSearchTerm(String term) {
// Whitelist alphanumeric and spaces
return term != null &&
term.length() <= 100 &&
term.matches("^[a-zA-Z0-9\\s]+$");
}
// SAFE: Whitelist document names
private static final Set<String> ALLOWED_DOCS = Set.of(
"products.xml", "categories.xml", "public.xml"
);
public String getDocument(String docName) {
// Validate document name
if (!ALLOWED_DOCS.contains(docName)) {
throw new SecurityException("Document not allowed");
}
String xquery = "declare variable $doc external; doc($doc)";
XQPreparedExpression expr = connection.prepareExpression(xquery);
expr.bindString(new QName("doc"), docName, null);
return executeQuery(expr);
}
}
# SAFE: Python XQuery with proper escaping and validation
import re
from xmldb import XMLDatabase
class SafeXMLDatabase:
def __init__(self):
self.db = XMLDatabase()
def escape_xquery_string(self, value):
"""Escape special characters for XQuery strings."""
if value is None:
return ''
# Escape quotes by doubling them
if "'" in value and '"' in value:
# Use concat for strings with both quote types
parts = value.split("'")
return "concat('" + "', \"'\", '".join(parts) + "')"
elif "'" in value:
return '"' + value + '"'
else:
return "'" + value + "'"
def validate_input(self, value, max_length=100):
"""Validate input against whitelist."""
if not value or len(value) > max_length:
return None
if not re.match(r'^[a-zA-Z0-9_\- ]+$', value):
return None
return value
def login(self, username, password):
# Validate inputs
username = self.validate_input(username)
password = self.validate_input(password)
if not username or not password:
return False
# SAFE: Using external variables
xquery = """
declare variable $user external;
declare variable $pass external;
for $u in doc('users.xml')//user
where $u/username = $user
and $u/password = $pass
return $u
"""
result = self.db.execute(xquery, {
'user': username,
'pass': password
})
return len(result) > 0
def search_products(self, category, min_price):
# Validate category
category = self.validate_input(category)
if not category:
raise ValueError("Invalid category")
# Validate price as number
try:
min_price = float(min_price)
if min_price < 0:
raise ValueError()
except (ValueError, TypeError):
raise ValueError("Invalid price")
# SAFE: Parameterized query
xquery = """
declare variable $cat external;
declare variable $price external;
for $p in doc('products.xml')//product
where $p/category = $cat
and $p/price >= $price
return $p/name/text()
"""
return self.db.execute(xquery, {
'cat': category,
'price': min_price
})
# SAFE: Whitelist for document access
ALLOWED_DOCS = {'products.xml', 'categories.xml', 'public.xml'}
def get_document(self, doc_name):
if doc_name not in self.ALLOWED_DOCS:
raise ValueError("Document not allowed")
xquery = """
declare variable $docname external;
doc($docname)
"""
return self.db.execute(xquery, {'docname': doc_name})
// SAFE: Node.js XQuery with parameterization
class SafeXQueryService {
constructor(db) {
this.db = db;
}
validateInput(value, maxLength = 100) {
if (!value || typeof value !== 'string') {
return null;
}
if (value.length > maxLength) {
return null;
}
if (!/^[a-zA-Z0-9_\- ]+$/.test(value)) {
return null;
}
return value;
}
async authenticate(username, password) {
// Validate inputs
username = this.validateInput(username);
password = this.validateInput(password);
if (!username || !password) {
return false;
}
// SAFE: Use parameterized query
const xquery = `
declare variable $user external;
declare variable $pass external;
for $u in doc('users.xml')//user
where $u/username = $user
and $u/password = $pass
return $u
`;
const result = await this.db.query(xquery, {
user: username,
pass: password
});
return result.length > 0;
}
async searchDocuments(keyword) {
// Validate keyword
keyword = this.validateInput(keyword);
if (!keyword) {
throw new Error('Invalid search keyword');
}
// SAFE: Parameterized search
const xquery = `
declare variable $term external;
for $doc in collection('documents')//document
where contains($doc/content, $term)
return $doc
`;
return await this.db.query(xquery, { term: keyword });
}
// SAFE: Whitelist allowed configurations
static ALLOWED_CONFIGS = new Set(['theme', 'language', 'timezone']);
async getConfiguration(configName) {
if (!SafeXQueryService.ALLOWED_CONFIGS.has(configName)) {
throw new Error('Configuration not allowed');
}
const xquery = `
declare variable $name external;
doc('config.xml')//config[@name=$name]
`;
return await this.db.query(xquery, { name: configName });
}
}
(: SAFE: XQuery with proper parameter handling :)
declare function local:authenticate-safe(
$username as xs:string,
$password as xs:string
) as element()? {
(: SAFE: Direct comparison with typed parameters :)
(: XQuery processor handles escaping :)
for $user in doc('users.xml')//user
where $user/username = $username
and $user/password = $password
return $user
};
(: SAFE: Input validation function :)
declare function local:validate-input($input as xs:string?) as xs:string? {
if (empty($input)) then ()
else if (string-length($input) > 100) then ()
else if (not(matches($input, '^[a-zA-Z0-9_\- ]+$'))) then ()
else $input
};
declare function local:search-safe($term as xs:string) as element()* {
let $validated := local:validate-input($term)
return
if (empty($validated)) then ()
else
for $item in doc('items.xml')//item
where contains($item/name, $validated)
return $item
};
Exploited in the Wild
Authentication Bypass
XQuery injection to bypass XML-based authentication.
Data Extraction
Extracting sensitive data from XML databases.
LDAP/XML Hybrid Attacks
Combined XQuery and LDAP injection.
Tools to test/exploit
-
XMLSpy — XQuery testing.
-
Custom injection scripts.
-
Burp Suite with XQuery payloads.
CVE Examples
-
CVEs in XML database products.
-
XQuery injection in content management systems.
References
-
MITRE. "CWE-652: Improper Neutralization of Data within XQuery Expressions." https://cwe.mitre.org/data/definitions/652.html
-
W3C. "XQuery Language Specification."