10. OWASP Top 10 — Injections et logique applicative#
Environnements isolés — Avertissement éthique
Tous les exemples d’exploitation présentés dans ce chapitre s’exécutent exclusivement sur des environnements isolés en mémoire (SQLite :memory:, serveurs de test Python). Aucune cible réelle, aucun système tiers, aucune infrastructure de production n’est sollicitée. L’objectif est pédagogique : comprendre les mécanismes pour mieux défendre.
Introduction#
Les injections constituent depuis plus de vingt ans la catégorie de vulnérabilité la plus répandue dans les applications web. Le principe est invariant : une entrée utilisateur est interprétée comme du code ou une commande par le système cible. L’OWASP Top 10 2021 classe les injections (A03) immédiatement après les défaillances de contrôle d’accès (A01) et les échecs cryptographiques (A02).
Injections SQL#
Mécanisme fondamental#
Une injection SQL survient quand une entrée utilisateur est concaténée dans une requête SQL sans assainissement. L’interpréteur SQL ne distingue pas les données des instructions.
Payloads SQL classiques
Ces payloads sont présentés à des fins éducatives. Ne les utilisez que sur des systèmes dont vous avez l’autorisation explicite.
' OR '1'='1— condition toujours vraie (bypass d’authentification)' OR 1=1--— commentaire SQL pour ignorer le reste de la requête'; DROP TABLE users;--— stacked query destructrice' UNION SELECT null,username,password FROM users--— UNION-based extraction' AND 1=2 UNION SELECT null,table_name,null FROM information_schema.tables--
Blind Injection#
Quand l’application ne retourne pas les données directement, deux techniques permettent d’extraire l’information bit à bit :
Boolean-based : la réponse diffère selon que la condition est vraie ou fausse.
' AND SUBSTRING(password,1,1)='a'-- → 200 OK (true)
' AND SUBSTRING(password,1,1)='b'-- → 404 / comportement différent (false)
Time-based : on provoque un délai conditionnel, observable dans le temps de réponse.
' AND IF(1=1, SLEEP(5), 0)-- → délai de 5 secondes
' AND IF(SUBSTRING(password,1,1)='a', SLEEP(3), 0)--
Défenses contre l’injection SQL#
Requêtes paramétrées (prepared statements) : l’entrée utilisateur est transmise hors bande — elle ne peut jamais être interprétée comme SQL.
ORM avec requêtes typées : SQLAlchemy, Hibernate, Django ORM utilisent des paramètres par défaut.
Validation et liste blanche des entrées attendues (types, formats, longueurs).
Principe du moindre privilège : le compte de base de données de l’application ne doit pas avoir accès aux tables système ni aux droits
DROP/ALTER.WAF comme couche défense en profondeur (non suffisant seul).
Autres injections#
LDAP Injection#
Les répertoires LDAP utilisent un langage de filtre propre. La concaténation directe permet de modifier les filtres de recherche.
Requête vulnérable : (&(uid= + utilisateur + )(userPassword= + mdp + ))
Payload : *)(uid=*))(|(uid=* → filtre résultant : (&(uid=*)(uid=*))(|(uid=*))(userPassword=...)
Command Injection (injection de commande OS)#
Quand une application appelle des fonctions système (system(), subprocess, exec) en incluant des entrées utilisateur :
Métacaractères shell dangereux
;, &&, ||, ` (backtick), $(), |, >, <, \n
Exemple vulnérable Python : os.system(f"ping -c 1 {host_input}")
Payload : 8.8.8.8; cat /etc/passwd
Protection : utiliser subprocess.run([cmd, arg], shell=False) — la liste d’arguments empêche l’interprétation des métacaractères.
Template Injection (SSTI)#
Les moteurs de template (Jinja2, Twig, FreeMarker) évaluent les expressions {{ }} ou ${...}. Si une entrée utilisateur est rendue sans échappement :
Jinja2 : {{ 7*7 }} → affiche 49 (preuve d’exécution de code)
Escalade : {{ ''.__class__.__mro__[1].__subclasses__()[X].__init__.__globals__['os'].popen('id').read() }}
XPath Injection#
Les requêtes XPath sur des documents XML sont vulnérables au même principe :
//user[name/text()='' or '1'='1' and password/text()='x' or '1'='1']
Injection NoSQL — MongoDB#
MongoDB utilise des opérateurs JSON. Des paramètres non typés permettent d’injecter des opérateurs :
{ "username": "admin", "password": { "$gt": "" } }
L’opérateur $gt (greater than) rend la condition toujours vraie. L’opérateur $where permet d’exécuter du JavaScript côté serveur (désactivé par défaut depuis MongoDB 4.4).
SSRF — Server-Side Request Forgery#
Exploitation#
Le SSRF (A10:2021) force le serveur à effectuer des requêtes HTTP en son nom vers des destinations choisies par l’attaquant. Vecteurs courants : import d’URL (avatar, webhook), PDF generators, fetch de métadonnées.
Cible cloud critique — AWS Instance Metadata Service :
http://169.254.169.254/latest/meta-data/iam/security-credentials/
Cette URL n’est accessible que depuis l’instance EC2 elle-même. Si l’application effectue la requête pour l’attaquant, les credentials IAM temporaires sont exposés.
Techniques de bypass de filtres#
Technique |
Exemple |
|---|---|
Encodage décimal |
|
Encodage hexadécimal |
|
Notation octale |
|
IPv6 loopback |
|
Redirection ouverte |
|
Protocoles alternatifs |
|
DNS rebinding |
Le FQDN résout d’abord vers une IP publique, puis vers 127.0.0.1 |
Path Traversal et LFI#
Path Traversal : inclusion de ../ dans un chemin de fichier pour sortir du répertoire racine.
GET /download?file=../../../../etc/passwd
Encodages de bypass :
URL encoding :
%2e%2e%2fDouble encoding :
%252e%252e%252fNull byte (anciennes versions PHP) :
../../../etc/passwd%00.jpgUnicode :
..%c0%af(surlong UTF-8, anciens serveurs IIS)
Protection : utiliser os.path.realpath() + vérification que le chemin résolu commence bien par le répertoire autorisé.
XXE — XML External Entity#
Les processeurs XML peuvent résoudre des entités externes définies dans le DTD (Document Type Definition). Si l’analyseur XML est mal configuré :
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE foo [
<!ENTITY xxe SYSTEM "file:///etc/passwd">
]>
<userdata><name>&xxe;</name></userdata>
Le contenu de /etc/passwd est injecté dans la réponse.
OOB XXE (Out-of-Band) : quand la réponse n’est pas retournée, les données sont exfiltrées via une requête DNS ou HTTP vers un serveur contrôlé par l’attaquant.
Protection XXE
Désactiver les DTD externes dans le processeur XML : en Python, utiliser defusedxml à la place de xml.etree.ElementTree. En Java, XMLInputFactory.setProperty(XMLInputFactory.SUPPORT_DTD, false).
Cellules Python exécutables#
Injection SQL : démonstration SQLite en mémoire#
# ──────────────────────────────────────────────────────────
# Création de la base de données en mémoire
# ──────────────────────────────────────────────────────────
conn = sqlite3.connect(":memory:")
cursor = conn.cursor()
cursor.execute("""
CREATE TABLE utilisateurs (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
password TEXT NOT NULL,
role TEXT DEFAULT 'user'
)
""")
cursor.executemany(
"INSERT INTO utilisateurs (username, password, role) VALUES (?, ?, ?)",
[
("alice", "secret_alice_hash", "user"),
("bob", "secret_bob_hash", "user"),
("admin", "super_secret_admin","admin"),
]
)
conn.commit()
# ──────────────────────────────────────────────────────────
# Version VULNÉRABLE : concaténation directe (f-string)
# ──────────────────────────────────────────────────────────
def login_vulnerable(username_input, password_input):
"""Authentification vulnérable à l'injection SQL."""
query = f"""
SELECT id, username, role FROM utilisateurs
WHERE username = '{username_input}'
AND password = '{password_input}'
"""
try:
rows = cursor.execute(query).fetchall()
return rows, query
except sqlite3.OperationalError as e:
return [], f"ERREUR SQL : {e}"
# ──────────────────────────────────────────────────────────
# Version SÉCURISÉE : requête paramétrée
# ──────────────────────────────────────────────────────────
def login_securise(username_input, password_input):
"""Authentification avec requête paramétrée."""
query = "SELECT id, username, role FROM utilisateurs WHERE username = ? AND password = ?"
rows = cursor.execute(query, (username_input, password_input)).fetchall()
return rows
# ──────────────────────────────────────────────────────────
# Tests
# ──────────────────────────────────────────────────────────
print("=" * 60)
print("SCÉNARIO 1 : Connexion légitime")
print("=" * 60)
res, q = login_vulnerable("alice", "secret_alice_hash")
print(f"Requête : {q.strip()}")
print(f"Résultat : {res}\n")
print("=" * 60)
print("SCÉNARIO 2 : Injection SQL — bypass d'authentification")
print("=" * 60)
payload_user = "' OR '1'='1"
payload_pass = "' OR '1'='1"
res, q = login_vulnerable(payload_user, payload_pass)
print(f"Input utilisateur : {repr(payload_user)}")
print(f"Input mot de passe : {repr(payload_pass)}")
print(f"Requête construite :\n{q.strip()}")
print(f"Résultat (TOUS les utilisateurs extraits) : {res}\n")
print("=" * 60)
print("SCÉNARIO 3 : Injection — extraction via commentaire SQL")
print("=" * 60)
payload_user2 = "admin'--"
payload_pass2 = "n_importe_quoi"
res, q = login_vulnerable(payload_user2, payload_pass2)
print(f"Input utilisateur : {repr(payload_user2)}")
print(f"Requête construite :\n{q.strip()}")
print(f"Résultat (accès admin sans mot de passe) : {res}\n")
print("=" * 60)
print("SCÉNARIO 4 : Version sécurisée — même payload rejeté")
print("=" * 60)
res_sec = login_securise(payload_user, payload_pass)
print(f"Résultat avec requête paramétrée : {res_sec}")
res_sec2 = login_securise(payload_user2, payload_pass2)
print(f"Résultat bypass admin : {res_sec2}")
print("\nConclusion : avec les requêtes paramétrées, aucun payload n'est interprété.")
============================================================
SCÉNARIO 1 : Connexion légitime
============================================================
Requête : SELECT id, username, role FROM utilisateurs
WHERE username = 'alice'
AND password = 'secret_alice_hash'
Résultat : [(1, 'alice', 'user')]
============================================================
SCÉNARIO 2 : Injection SQL — bypass d'authentification
============================================================
Input utilisateur : "' OR '1'='1"
Input mot de passe : "' OR '1'='1"
Requête construite :
SELECT id, username, role FROM utilisateurs
WHERE username = '' OR '1'='1'
AND password = '' OR '1'='1'
Résultat (TOUS les utilisateurs extraits) : [(1, 'alice', 'user'), (2, 'bob', 'user'), (3, 'admin', 'admin')]
============================================================
SCÉNARIO 3 : Injection — extraction via commentaire SQL
============================================================
Input utilisateur : "admin'--"
Requête construite :
SELECT id, username, role FROM utilisateurs
WHERE username = 'admin'--'
AND password = 'n_importe_quoi'
Résultat (accès admin sans mot de passe) : []
============================================================
SCÉNARIO 4 : Version sécurisée — même payload rejeté
============================================================
Résultat avec requête paramétrée : []
Résultat bypass admin : []
Conclusion : avec les requêtes paramétrées, aucun payload n'est interprété.
Simulation d’un validateur SSRF#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.1)
# ──────────────────────────────────────────────────────────
# Implémentation d'un validateur d'URL côté serveur
# Approche blocklist (faible) vs allowlist (robuste)
# ──────────────────────────────────────────────────────────
# IPs et plages bloquées en SSRF (RFC 1918, métadonnées cloud, loopback)
PLAGES_BLOQUEES = [
ipaddress.ip_network("127.0.0.0/8"), # Loopback
ipaddress.ip_network("10.0.0.0/8"), # RFC 1918
ipaddress.ip_network("172.16.0.0/12"), # RFC 1918
ipaddress.ip_network("192.168.0.0/16"), # RFC 1918
ipaddress.ip_network("169.254.0.0/16"), # Link-local / AWS metadata
ipaddress.ip_network("::1/128"), # IPv6 loopback
ipaddress.ip_network("fc00::/7"), # IPv6 ULA
]
DOMAINES_AUTORISES = {"api.alkimya.fr", "cdn.alkimya.fr", "storage.alkimya.fr"}
def ip_est_bloquee(ip_str):
"""Vérifie si une IP appartient à une plage bloquée."""
try:
ip = ipaddress.ip_address(ip_str)
return any(ip in plage for plage in PLAGES_BLOQUEES)
except ValueError:
return False
def valider_url_blocklist(url):
"""
Approche blocklist : bloque les IP connues dangereuses.
FAIBLE : contournable par encodage, DNS rebinding, redirections.
"""
try:
parsed = urlparse(url)
host = parsed.hostname
if not host:
return False, "Hôte invalide"
# Vérifie si c'est une IP directe
try:
if ip_est_bloquee(host):
return False, f"IP bloquée : {host}"
except ValueError:
pass # C'est un FQDN, pas une IP
return True, "Autorisé (blocklist)"
except Exception as e:
return False, f"Erreur : {e}"
def valider_url_allowlist(url):
"""
Approche allowlist : n'autorise que les domaines explicitement approuvés.
ROBUSTE : toute URL non listée est rejetée par défaut.
"""
try:
parsed = urlparse(url)
host = parsed.hostname
if not host:
return False, "Hôte invalide"
if host not in DOMAINES_AUTORISES:
return False, f"Domaine non autorisé : {host}"
if parsed.scheme not in ("https",):
return False, "Schéma non autorisé (HTTPS uniquement)"
return True, "Autorisé (allowlist)"
except Exception as e:
return False, f"Erreur : {e}"
# ──────────────────────────────────────────────────────────
# Jeu de test
# ──────────────────────────────────────────────────────────
cas_test = [
# (description, url)
("URL légitime HTTPS", "https://api.alkimya.fr/data"),
("URL légitime CDN", "https://cdn.alkimya.fr/img.png"),
("Domaine externe non autorisé", "https://evil.com/payload"),
("Métadonnées AWS (IP directe)", "http://169.254.169.254/latest/meta-data/"),
("Loopback explicite", "http://127.0.0.1/admin"),
("Réseau interne RFC 1918", "http://192.168.1.1/config"),
("Encoding décimal loopback", "http://2130706433/"),
("Encoding hexadécimal loopback", "http://0x7f000001/"),
("IPv6 loopback", "http://[::1]/"),
("Schéma file:// LFI", "file:///etc/passwd"),
("Redirection ouverte vers metadata", "https://api.alkimya.fr/redir?url=http://169.254.169.254/"),
]
print(f"{'Description':<40} {'Blocklist':<25} {'Allowlist':<25}")
print("-" * 90)
for desc, url in cas_test:
b_ok, b_msg = valider_url_blocklist(url)
a_ok, a_msg = valider_url_allowlist(url)
b_sym = "✓" if b_ok else "✗"
a_sym = "✓" if a_ok else "✗"
print(f"{desc:<40} {b_sym} {b_msg:<22} {a_sym} {a_msg:<22}")
Description Blocklist Allowlist
------------------------------------------------------------------------------------------
URL légitime HTTPS ✓ Autorisé (blocklist) ✓ Autorisé (allowlist)
URL légitime CDN ✓ Autorisé (blocklist) ✓ Autorisé (allowlist)
Domaine externe non autorisé ✓ Autorisé (blocklist) ✗ Domaine non autorisé : evil.com
Métadonnées AWS (IP directe) ✗ IP bloquée : 169.254.169.254 ✗ Domaine non autorisé : 169.254.169.254
Loopback explicite ✗ IP bloquée : 127.0.0.1 ✗ Domaine non autorisé : 127.0.0.1
Réseau interne RFC 1918 ✗ IP bloquée : 192.168.1.1 ✗ Domaine non autorisé : 192.168.1.1
Encoding décimal loopback ✓ Autorisé (blocklist) ✗ Domaine non autorisé : 2130706433
Encoding hexadécimal loopback ✓ Autorisé (blocklist) ✗ Domaine non autorisé : 0x7f000001
IPv6 loopback ✗ IP bloquée : ::1 ✗ Domaine non autorisé : ::1
Schéma file:// LFI ✗ Hôte invalide ✗ Hôte invalide
Redirection ouverte vers metadata ✓ Autorisé (blocklist) ✓ Autorisé (allowlist)
Blocklist vs Allowlist pour SSRF
La blocklist rate les encodages alternatifs d’IP (décimal, hexadécimal, IPv6), les DNS rebinding et les redirections ouvertes. L’allowlist en liste blanche de domaines approuvés est la seule approche véritablement robuste. En environnement cloud, compléter avec un IMDS v2 (IMDSv2) qui requiert un token de session.
Heatmap OWASP Top 10 2021#
sns.set_theme(style="whitegrid", palette="muted", font_scale=1.0)
# Scores OWASP Top 10 2021 (échelle 0–10)
# Sources : rapport OWASP 2021, données CVE, jugement d'expert
categories = [
"A01 Broken Access Control",
"A02 Cryptographic Failures",
"A03 Injection",
"A04 Insecure Design",
"A05 Security Misconfiguration",
"A06 Vulnerable Components",
"A07 Auth Failures",
"A08 Software & Data Integrity",
"A09 Security Logging Failures",
"A10 SSRF",
]
dimensions = ["Fréquence", "Impact", "Exploitabilité"]
# Scores (0-10) : fréquence d'occurrence, gravité de l'impact, facilité d'exploitation
scores = np.array([
[9.0, 8.5, 8.0], # A01 Broken Access Control
[8.0, 9.0, 6.0], # A02 Cryptographic Failures
[8.5, 8.5, 8.5], # A03 Injection
[6.0, 8.0, 5.5], # A04 Insecure Design
[8.5, 7.0, 8.0], # A05 Security Misconfiguration
[7.5, 7.5, 7.0], # A06 Vulnerable Components
[7.0, 8.5, 7.5], # A07 Auth Failures
[5.5, 8.0, 6.0], # A08 Software & Data Integrity
[7.0, 6.0, 5.0], # A09 Security Logging Failures
[5.0, 8.0, 7.0], # A10 SSRF
])
fig, ax = plt.subplots(figsize=(10, 7))
sns.heatmap(
scores,
annot=True,
fmt=".1f",
cmap="YlOrRd",
xticklabels=dimensions,
yticklabels=[c.replace(" ", "\n", 1) for c in categories],
linewidths=0.5,
linecolor="white",
vmin=0, vmax=10,
cbar_kws={"label": "Score (0–10)"},
ax=ax
)
ax.set_title("OWASP Top 10 2021 — Fréquence × Impact × Exploitabilité", fontsize=12, fontweight="bold", pad=15)
ax.set_xlabel("Dimension d'évaluation", labelpad=10)
ax.set_ylabel("Catégorie OWASP", labelpad=10)
ax.tick_params(axis="y", labelsize=8)
plt.show()
Résumé#
L’injection SQL reste la vulnérabilité la plus exploitée. La seule protection fiable est l’utilisation systématique de requêtes paramétrées. Les payloads classiques (OR 1=1, UNION SELECT, stacked queries) deviennent inoffensifs lorsque les données sont séparées du code SQL.
Les injections de commande (OS, LDAP, XPath, template) suivent le même principe : entrée interprétée comme code. La défense universelle est la validation stricte des entrées et l’évitement de l’interprétation dynamique.
Le SSRF exploite le fait que le serveur est un proxy non contrôlé. Une allowlist de domaines autorisés, combinée à la désactivation de l’IMDS v1 (AWS) ou son équivalent, est la contre-mesure principale.
XXE est éliminé en désactivant les DTD externes dans le parseur XML. La bibliothèque
defusedxml(Python) couvre tous les vecteurs d’attaque XML courants.Le path traversal se contrecarre par normalisation des chemins (
realpath()) et validation de préfixe avant tout accès fichier.NoSQL n’est pas immune aux injections : les opérateurs MongoDB (
$gt,$where,$regex) sont exploitables si les entrées ne sont pas typées. Utiliser des schémas de validation stricts (Mongoose, JSON Schema).La heatmap OWASP illustre que les vulnérabilités les plus fréquentes ne sont pas toujours les plus exploitables, et vice versa. Une stratégie de remédiation efficace priorise la combinaison fréquence × impact × exploitabilité.