Open redirect (CWE-601) : un attaquant détourne un paramètre de redirection pour envoyer les utilisateurs vers des URLs contrôlées — phishing, vol de tokens OAuth et escalade vers SSRF.
TL;DR
?next=, ?redirect=, ?goto= ; OAuth redirect_uri ; JavaScript window.location\\, %09, @endsWith ni correspondance par préfixeUne redirection ouverte est une classe de vulnérabilité sous CWE-601 (URL Redirection to Untrusted Site) où une application accepte une URL dans un paramètre de requête et redirige le navigateur vers cette destination sans valider que la cible est un hôte autorisé. Le mécanisme de redirection existe pour des raisons légitimes — renvoyer l'utilisateur vers la page qu'il essayait d'atteindre après connexion, ou le retourner vers l'application appelante après un flux OAuth. La vulnérabilité apparaît quand l'URL de destination est prise telle quelle depuis une entrée contrôlée par l'attaquant.
La forme serveur émet un HTTP 302 (ou 301, 303, 307) avec un header Location contenant l'URL de l'attaquant. Le navigateur le suit automatiquement. La forme côté client utilise JavaScript : window.location = valeurParam. Les deux relèvent d'OWASP A01:2021 (Broken Access Control) car elles permettent de rediriger les utilisateurs hors de la limite de contrôle de l'application.
Une redirection ouverte obtient CVSS 6.1 en isolation — Moyen, souvent noté Faible dans les programmes avec des seuils élevés. Le vrai multiplicateur de gravité vient du chaînage. Une redirection ouverte sur un endpoint d'autorisation OAuth devient CVSS 9.8 (CVE-2021-29156, ForgeRock OpenAM). Une redirection ouverte sur un domaine dans une allowlist SSRF devient CVSS 8.6 (CVE-2024-2376, LangChain). Selon les données HackerOne Hacktivity, les redirections ouvertes représentent environ 6% des reports valides.
Le motif de base est une redirection côté serveur via paramètre :
GET /login?next=https://attacker.com HTTP/1.1
Host: banque-de-confiance.com
HTTP/1.1 302 Found
Location: https://attacker.comLe navigateur suit le header Location vers attacker.com. L'utilisateur voit banque-de-confiance.com dans sa barre d'adresse pendant la requête initiale — la confiance est établie avant que la redirection se produise.
La chaîne de redirection se déroule en trois étapes :
req.query.next et le passe directement à res.redirect() sans validation d'hôte.| Variante | Technique | CVSS (isolé) | Chaîne élevée |
|---|---|---|---|
| Paramètre URL côté serveur | ?next=https://evil.com dans Location 30x | 6.1 | 8.6 avec SSRF |
| Header (Host/X-Forwarded-Host) | Override Host → construction URL redirection | 6.1 | 8.1 avec reset poisoning |
| JavaScript DOM | window.location = paramètre | 4.3 | 7.4 avec vol cookie |
OAuth redirect_uri | Bypass exact match → vol de code | 8.6 | 9.8 (ATO complet) |
| Chaîne SSRF | Domaine de confiance redirige vers IMDS | 8.6 | 9.1 avec identifiants cloud |
| Chemin (path-based) | /go/https://evil.com | 6.1 | 7.5 |
from urllib.parse import urlparse
# Python voit ceci comme un chemin sans netloc — "sûr"
urlparse("\\evil.com").netloc # Retourne '' — Python pense que netloc est vide
urlparse("%09https://evil.com").netloc # Retourne '' — tab supprimé, pas de schéma
# Mais WHATWG (Chrome/Firefox) normalise :
# \\evil.com → //evil.com → https://evil.com (navigateur suit)
# %09https://evil.com → https://evil.com (navigateur décode le tab, analyse l'URL absolue)Payloads de bypass actifs (liste Diverto 2024) :
//attacker.com # Protocol-relative — les deux slashes requis
https:////attacker.com # Slashes multiples — WHATWG élimine les extras
https:\attacker.com # Antislash — WHATWG normalise en /
https://trusted.com@evil.com # Userinfo — trusted.com est le nom d'utilisateur
https://trusted.com.evil.com # Abus de suffixe — valide l'hostname complet, pas endsWith
%EF%BC%8E # Point pleine largeur Unicode — normalisation → .url.startsWith("https://trusted.com") est contournable avec https://trusted.com.evil.com et https://trusted.com@evil.com. Ne validez jamais les destinations de redirection par préfixe ou suffixe de chaîne. Parsez l'URL, extrayez le hostname, et comparez contre une allowlist d'exacte correspondance.
CVE-2025-4123 — Grafana (CVSS 7.6)
L'endpoint /public/dashboards/ de Grafana traitait la chaîne de requête comme une cible de redirection via une confusion de parsing chemin/requête. GET /public/dashboards/?attacker.com émettait une redirection vers attacker.com. Combiné avec la gestion des cookies de session de Grafana, cela permettait une prise de contrôle de compte. CVE-2025-6023 a suivi comme bypass du correctif initial, démontrant que la validation par chaîne sans normalisation WHATWG est insuffisante.
CVE-2025-69725 — go-chi RedirectSlashes (CVSS 6.1)
Le middleware RedirectSlashes de go-chi normalisait les chemins à double slash : GET //evil.com était traité comme un chemin nécessitant normalisation, produisant 302 Location: //evil.com. Les navigateurs résolvent les URLs protocol-relative (//evil.com) en https://evil.com. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N.
CVE-2024-23832 — Mastodon (CVSS 9.4)
Le flux de suivi distant de Mastodon acceptait des valeurs redirect_uri non validées. Chaîné avec la gestion des tokens OAuth, les attaquants pouvaient voler des tokens de compte Mastodon. Le score CVSS de 9.4 reflète la chaîne de prise de contrôle de compte, pas juste la redirection isolée.
CVE-2024-2376 — LangChain SSRF via Redirection (CVSS 8.6)
Le chargeur de documents URL de LangChain suivait les redirections HTTP sans valider la destination finale. Une URL pointant vers un domaine de confiance ayant une redirection ouverte permettait au chargeur d'atteindre http://169.254.169.254/latest/meta-data/iam/security-credentials/ — obtenant des identifiants IAM AWS depuis le service de métadonnées d'instance.
CVE-2021-29156 — ForgeRock OpenAM (CVSS 9.8)
Le paramètre goto dans le flux d'authentification de ForgeRock acceptait des URLs externes arbitraires. Pendant l'autorisation OAuth, le code d'autorisation était ajouté à l'URL goto avant la redirection — permettant l'exfiltration des codes d'autorisation. CVSS 9.8 car la chaîne contournait l'authentification entièrement.
HackerOne #112614 — Twitter OAuth (1 470 $)
Le flux OAuth callback de Twitter avait une redirection ouverte permettant l'interception des tokens d'accès OAuth via un redirect_uri forgée. Ce report de 2015 reste la référence canonique en bug bounty démontrant que les redirections ouvertes dans un contexte OAuth méritent un traitement haute gravité.
gf redirect (tomnomnom/gf) contre les données Wayback Machine.Location pour du contenu fourni par l'utilisateur.https://example.com d'abord, puis votre URL Burp Collaborator ou Interactsh.redirect_uri et observer si le code d'autorisation est ajouté à l'URL forgée avant la redirection.//attacker.com, \attacker.com, %09https://attacker.com, https://attacker.com@trusted.com, https://trusted.com.attacker.com.Location: /dashboard et Location: https://target.com/path ne sont PAS des redirections ouvertes.# OpenRedireX — fuzzer async avec 50+ payloads de bypass
echo "https://target.com/login?next=FUZZ" | openredirex -p /path/to/payloads.txt
# nuclei — templates CVE + fuzzing de paramètres
nuclei -u https://target.com -t fuzzing/redirect-params.yaml
nuclei -u https://target.com -t cves/2025/CVE-2025-4123.yaml
# Dalfox v3+ — mode open-redirect
dalfox url "https://target.com/login?next=1" --open-redirectBreachVex détecte les redirections ouvertes via des sondes canary out-of-band injectées dans les paramètres de redirection découverts. Un finding confirmé nécessite un header Location 30x contenant le domaine canary, plus une vérification eTLD+1 pour éliminer les faux positifs same-origin.
# Flask — BONNE PRATIQUE
from urllib.parse import urlparse
ALLOWED_HOSTS = frozenset({"app.example.com", "dashboard.example.com"})
def is_safe_redirect(url: str, fallback: str = "/dashboard") -> str:
if not url:
return fallback
# Rejeter les caractères de contrôle
if any(c in url for c in ["\r", "\n", "\t", "\x00", "\\"]):
return fallback
parsed = urlparse(url)
# Autoriser les URLs relatives commençant par / (pas //)
if not parsed.netloc and url.startswith("/") and not url.startswith("//"):
return url
# Les URLs externes doivent être sur l'allowlist
if parsed.netloc not in ALLOWED_HOSTS:
return fallback
return url// Express.js — BONNE PRATIQUE
const ALLOWED_HOSTS = new Set(["app.example.com", "dashboard.example.com"]);
function isSafeRedirect(url) {
if (!url) return false;
if (url.startsWith("/") && !url.startsWith("//")) return true;
try {
// Constructeur WHATWG — même parseur que les navigateurs
const parsed = new URL(url);
return ALLOWED_HOSTS.has(parsed.hostname);
} catch {
return false;
}
}
app.get("/redirect", (req, res) => {
const next = req.query.next ?? "/home";
res.redirect(isSafeRedirect(next) ? next : "/home");
});# Stocker la destination avant la redirection — aucune URL dans le paramètre
import secrets
import redis
redis_client = redis.Redis()
@app.route("/pre-login")
def pre_login():
state = secrets.token_urlsafe(32)
redis_client.set(f"redirect:{state}", "/user/dashboard", ex=300)
return redirect(f"/login?state={state}")
@app.route("/post-login")
def post_login():
state = request.args.get("state", "")
raw = redis_client.get(f"redirect:{state}")
destination = raw.decode() if raw else "/dashboard"
redis_client.delete(f"redirect:{state}")
return redirect(destination)# MAUVAISE PRATIQUE — correspondance par préfixe (contournable)
def validate_redirect_uri(requested: str, registered: str) -> bool:
return requested.startswith(registered)
# BONNE PRATIQUE — comparaison exacte de chaîne RFC 6749 §3.1.2
def validate_redirect_uri(requested: str, registered: str) -> bool:
return requested == registered # Correspondance exacte uniquementL'enregistrement wildcard redirect_uri (*.example.com) est une misconfiguration critique. Un attaquant peut enregistrer evil.example.com (si l'enregistrement de sous-domaine est possible) ou exploiter toute redirection ouverte sur n'importe quel sous-domaine. La RFC 6749 §3.1.2 interdit explicitement la correspondance par wildcard.
Une redirection ouverte (CWE-601) se produit quand une application accepte une URL contrôlée par l'utilisateur et redirige le navigateur vers cette destination sans vérifier qu'elle est autorisée. La forme la plus simple : /login?next=https://evil.com — le serveur émet un 302 Location: https://evil.com et le navigateur suit. La confiance vient du domaine d'origine ; le danger réside dans la destination.
Le CVSS de base est 6.1 (Moyen). En isolation, une redirection ouverte est notée Faible à Moyen. La gravité se multiplie par chaînage : une redirection ouverte sur un endpoint OAuth atteint CVSS 8.6-9.8 car elle permet le vol de codes d'autorisation. Dans la variante chaînée avec SSRF, le CVSS atteint 8.6+ avec accès potentiel aux identifiants cloud, comme CVE-2024-2376 (LangChain).
La redirection serveur émet une réponse HTTP 30x avec un header Location pointant vers l'URL de l'attaquant — le navigateur suit automatiquement. La redirection client (DOM-based) utilise JavaScript : window.location = valeurContrôlée. La redirection se produit dans le navigateur sans aller-retour serveur. Les deux relèvent de CWE-601 ; la variante DOM est plus difficile à détecter automatiquement car la logique est en JavaScript.
Dans OAuth 2.0, le serveur d'autorisation ajoute le code d'autorisation au redirect_uri avant d'y rediriger le navigateur. Si redirect_uri contient une redirection ouverte sur un domaine de confiance (ex: https://trusted.com/redirect?next=https://evil.com), le code est ajouté à cette URL. Le navigateur suit vers evil.com avec le code en paramètre — l'attaquant l'échange contre des tokens d'accès. CVE-2021-29156 (ForgeRock OpenAM, CVSS 9.8) est l'exemple canonique.
Des parseurs différents interprètent la même chaîne différemment. WHATWG (Chrome, Firefox) normalise les antislashs en slashs — //evil.com et \evil.com se résolvent tous deux en https://evil.com. Python urllib.parse traite \evil.com comme un chemin relatif. Node.js legacy url.parse traite //evil.com comme protocol-relative. Un backend qui valide avec Python peut accepter \evil.com comme 'sûr' tandis que le navigateur le résout vers un domaine externe.
Les défenses SSRF autorisent souvent des domaines de confiance spécifiques. Si le serveur récupère une URL d'un domaine de confiance, et que ce domaine a une redirection ouverte, le serveur suit la redirection vers un endpoint interne ou cloud. Dans CVE-2024-2376 (LangChain, CVSS 8.6), le chargeur d'URL suivait les redirections sans vérifier la destination finale — trusted.com?redirect=http://169.254.169.254/latest/meta-data/iam/security-credentials/ retournait des identifiants IAM AWS.
En priorité : next, redirect, redirect_uri, redirect_url, url, goto, target, dest, destination, return, returnTo, forward, link, continue, callback, success, failure, r, u. Endpoints suspects : /logout, /login, /oauth/authorize, /sso, /redirect, /go, /out, /external, /r/<id>. Vérifiez aussi le header Location dans toutes les réponses 30x pour du contenu utilisateur reflété.
Parsez le header Location et extrayez l'eTLD+1. Si l'eTLD+1 est identique à celui de la cible, ce n'est pas une redirection ouverte — les redirections vers le même domaine sont légitimes. Excluez aussi les domaines CDN (*.cloudfront.net, *.akamaiedge.net) appartenant à la cible et les partenaires OAuth déclarés. Une redirection ouverte confirmée a Location: https://<eTLD+1-externe-non-autorisé> avec un statut 30x.
La RFC 6749 §3.1.2 exige que la comparaison redirect_uri soit une correspondance exacte de chaîne. Pas de correspondance par préfixe, pas de wildcard, pas de tolérance pour les traversées de chemin. L'URI complet incluant schéma, hôte, chemin et chaîne de requête doit correspondre caractère par caractère. Forgejo CVE-2025-30215 et de nombreux CVE de bibliothèques OAuth résultent d'une implémentation de correspondance par préfixe au lieu d'égalité exacte.
Le middleware RedirectSlashes de go-chi normalisait les chemins à double slash. Une requête GET //evil.com était interprétée comme un chemin nécessitant normalisation, produisant 302 Location: //evil.com. Les navigateurs résolvent les URLs protocol-relative — //evil.com devient https://evil.com. Toute application Go utilisant chi router avec RedirectSlashes activé était affectée. CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N (6.1).
L'endpoint /public/dashboards/ de Grafana gérait mal le parsing de la chaîne de requête. Une requête GET /public/dashboards/?attacker.com produisait une redirection intégrant la chaîne de requête comme composant URL. Combiné avec la gestion des cookies de session de Grafana, cela permettait une prise de contrôle de compte. CVE-2025-6023 a suivi comme bypass du correctif initial, démontrant que la validation par chaîne brute sans normalisation WHATWG est insuffisante.
Python : utilisez urllib.parse.urlparse pour extraire le hostname, puis vérifiez hostname == 'expected.com' (pas endswith). Rejetez toute URL où le schéma n'est pas http/https et le netloc n'est pas sur l'allowlist. Node.js/Express : utilisez le constructeur URL (new URL(input)), vérifiez url.hostname contre un Set explicite de noms d'hôte autorisés. N'utilisez jamais string.startsWith ni string.includes pour la validation — ces méthodes sont contournées par l'abus userinfo (https://allowed.com@evil.com).
Une redirection ouverte CWE-601 isolée est CVSS 6.1. Chaînée avec vol de tokens OAuth, le CVSS monte à 8.6-9.8 selon l'impact. CVE-2021-29156 ForgeRock était CVSS 9.8 car il contournait l'authentification entièrement. CVE-2024-23832 Mastodon était 9.4. Le multiplicateur de chaîne est la clé — évaluez toujours la chaîne la plus sévère, pas la redirection isolée.
BreachVex utilise une stratégie de sondes out-of-band (OOB) : des URLs canary uniques sont injectées dans les paramètres de redirection découverts. Une redirection ouverte confirmée nécessite une réponse 30x avec Location contenant le domaine canary, ou un callback DNS/HTTP confirmant la récupération côté serveur. Le scan applique la normalisation eTLD+1 pour éliminer les faux positifs same-origin et vérifie la liste des partenaires CDN/OAuth avant d'émettre un finding.