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

ImpactDetails
ConfidentialityScope: Data Disclosure

Unauthorized access to XML data.
AuthenticationScope: Bypass

Authentication logic circumvented.
IntegrityScope: 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

  1. MITRE. "CWE-652: Improper Neutralization of Data within XQuery Expressions." https://cwe.mitre.org/data/definitions/652.html

  2. W3C. "XQuery Language Specification."