Un open redirect est une page de votre site qui envoie le navigateur du visiteur vers une URL issue d'une saisie utilisateur, sans la vérifier. Cela paraît anodin. En pratique, un attaquant peut ainsi diffuser un lien de phishing qui affiche votre véritable domaine, et dans un contexte OAuth, la faille peut faire fuiter des codes d'autorisation et des jetons d'accès.
Pourquoi une redirection est plus qu'un défaut cosmétique
CWE-601 décrit cette faiblesse ainsi : une application web accepte une entrée contrôlée par l'utilisateur qui désigne un lien vers un site externe, puis utilise ce lien dans une redirection. La victime suit un lien vers votre site de confiance et atterrit sur une page contrôlée par l'attaquant. L'aide-mémoire de l'OWASP en donne la forme :
http://example.com/example.php?url=http://malicious.example.com
Le nom de serveur dans le lien modifié est identique à celui du site d'origine, ce qui rend la tentative de phishing plus crédible. Pour le point de vue de l'utilisateur, consultez notre vérificateur de liens.
Avec OAuth, la même faille porte plus loin. La RFC 9700 indique que les clients et les serveurs d'autorisation NE DOIVENT PAS exposer d'URL qui renvoient le navigateur vers des URI arbitraires tirées d'un paramètre de requête, car les open redirectors peuvent permettre l'exfiltration de codes d'autorisation et de jetons d'accès.
Les forwards constituent un cas voisin. Si une saisie utilisateur choisit une destination interne, comme dans function.jsp?fwd=admin.jsp, une URL forgée peut passer le contrôle d'accès et atteindre une fonction d'administration. L'application doit vérifier que l'utilisateur est autorisé à accéder à la cible.
Repérer les paramètres
Les noms varient : url, fwd et returnUrl apparaissent dans les exemples de l'OWASP, et votre code en contient d'autres. Cherchez le schéma, pas le nom.
| Stack | Schéma dangereux |
|---|---|
| Java | response.sendRedirect(request.getParameter("url")) |
| PHP | header("Location: " . $redirect_url) avec $_GET['url'] |
| C# .NET | Response.Redirect(url) avec request.QueryString["url"] |
| Rails | redirect_to params[:url] |
| Java forward | getRequestDispatcher(fwd).forward(...) |
Un premier passage avec grep vous donne une liste à relire. Adaptez le chemin et les types de fichiers à votre dépôt.
grep -rnE 'sendRedirect\(|Redirect\(|redirect_to params|header::LOCATION' src/
grep -rn -A3 'header(.Location' src/
La seconde commande concerne PHP. Lisez les lignes qui suivent chaque résultat : sans exit après header("Location..., le reste du script continue de s'exécuter. Un navigateur configuré pour ignorer la redirection peut alors afficher le reste de la page.
Les équipes .NET devraient examiner en premier les actions de connexion. Selon l'aide-mémoire, les sites ASP.NET MVC 1 et 2 sont particulièrement vulnérables, et son exemple LogOn de MVC 2 redirige vers returnUrl sans aucune validation. Sa correction consiste à migrer vers MVC 3.
La plupart des résultats seront inoffensifs. Pour toute redirection dont l'argument n'est ni un littéral ni une recherche côté serveur, posez une question : d'où vient la valeur ? Cookies, champs de formulaire cachés, en-têtes et contenus de base de données comptent tous. De nombreux open redirects existent parce que le développeur a supposé qu'un cookie ou un champ caché ne pouvait pas être modifié.
Pour confirmer une découverte, ne vous arrêtez pas à une simple adresse externe. Envoyez chacune des valeurs suivantes dans le paramètre et observez l'en-tête Location :
http://malicious.example.comhttps://yoursite.example@malicious.example(userinfo :yoursite.exampledevient un nom d'utilisateur)https://yoursite.example.malicious.example(un nom d'hôte qui commence par le vôtre)https://malicious-yoursite.example(un nom d'hôte qui se termine par le vôtre)
L'aide-mémoire cite le userinfo et les noms d'hôte trompeurs comme moyens de contourner les contrôles par préfixe et par suffixe. Si l'une de ces valeurs passe alors que l'URL simple était bloquée, le contrôle repose probablement sur une comparaison de chaînes. C'est une déduction, pas une preuve, mais remplacez le contrôle dans tous les cas.
Les scanners en détecteront certains. Le OWASP Web Security Testing Guide précise que ces outils sont génériques et manquent de contexte propre à l'application : vérifiez donc chaque résultat à la main.
Les corriger, par ordre de préférence
- Ne redirigez pas sur la base d'une saisie utilisateur. Si la destination est figée dans le code, un attaquant ne peut pas la modifier.
- Associez des ID à des cibles côté serveur. L'utilisateur fournit un nom court ou un ID, le serveur retrouve l'URL complète et rejette tout le reste. Veillez à ce que les ID ne puissent pas être parcourus en boucle, sinon un visiteur peut énumérer toutes les cibles.
- Utilisez l'assistant du framework pour les retours locaux. Dans ASP.NET Core, il s'agit de
LocalRedirectouIsLocalUrl, qui rejettent les destinations non locales. - Utilisez une liste d'autorisation pour les destinations externes. Servez-vous d'un parseur d'URL maintenu et comparez le schéma, l'hôte canonique et le port effectif analysés à une liste explicite. Restreignez le chemin si seuls certains points d'accès sont autorisés.
- Facultatif : exigez un nonce. CWE décrit un nonce imprévisible, généré par l'application, sur chaque requête de redirection, les requêtes qui en sont dépourvues étant refusées. Une faille XSS peut le contourner : il complète donc une correction sans la remplacer.
Règles pour le contrôle par liste d'autorisation :
- Rejetez le userinfo et les entrées ambiguës.
- Redirigez avec l'URL validée, pas avec une version que vous transformez ensuite.
- N'utilisez jamais de comparaison brute par préfixe ou par suffixe. L'aide-mémoire cite GitLab comme un cas où le userinfo et des noms d'hôte trompeurs ont mis en échec de tels contrôles.
Une liste de valeurs interdites constitue une défense principale faible, même si CWE l'accepte pour détecter des attaques. Préférez le principe « accepter uniquement ce qui est connu comme valide ».
Si des utilisateurs doivent quitter votre site, une page intermédiaire qui nomme la destination et exige un clic ajoute une couche supplémentaire. Évitez toute faille XSS lors de la construction de cette page.
Le volet OAuth
La RFC 9700 impose aux serveurs d'autorisation de comparer les URI de redirection des clients à celles préenregistrées par correspondance exacte de chaîne, sauf pour les numéros de port dans les URI de redirection localhost des applications natives. Ma lecture, qui n'est pas le texte de la RFC : la correspondance exacte prouve que le callback est bien celui enregistré, donc si cet hôte enregistré expose aussi un open redirector, le code peut être transmis plus loin. La RFC renvoie à sa section 4.11 pour le détail. Lisez-la avant de vous fier à mon résumé.
Par où commencer
- Exécutez les motifs grep ci-dessus sur votre code et listez chaque redirection et chaque forward dont la cible provient d'une requête, d'un cookie ou d'une valeur stockée. Vérifiez en priorité les gestionnaires de connexion et de callback OAuth.
- Vérifiez l'absence de
exitdans les résultats PHP, et recherchez dans les projets .NET les actions de connexion MVC 1 ou 2. - Testez chaque résultat avec les quatre valeurs ci-dessus, y compris la forme userinfo, et notez lesquelles envoient le navigateur hors de votre domaine.
- Remplacez les URL fournies par l'utilisateur par un mappage d'ID côté serveur lorsque c'est possible, par des assistants de framework limités au local pour les adresses de retour, et par une liste d'autorisation sur URL analysée pour les rares destinations externes nécessaires.
- Passez en revue les enregistrements de vos clients OAuth pour vérifier que les URI de redirection sont exactes, et confirmez qu'aucun de vos hôtes n'expose d'open redirector.
- Ajoutez un test de redirection à vos revues de code et au périmètre de vos tests d'intrusion, afin que la faille ne revienne pas.
