← All write-ups
January 8, 2024HIGHFINDING MISSION

XSS reflété sur un paramètre de redirection post-login

Finding sur une application RH (secteur public, client/éditeur anonymisés). Découvert par 34zY / Hamza Terir. Divulgation tentée auprès de l’éditeur, restée sans réponse. Domaines et noms produit retirés.

Contexte

Application RH avec une page de connexion utilisant un paramètre de redirection post-login (type redirect / return_to) destiné à renvoyer l’utilisateur vers une page après authentification.

Comment je l’ai trouvée

Le paramètre de redirection est réinjecté directement dans la valeur d’un champ caché du formulaire, sans encodage :

<input type="hidden" name="redirect" value="[ENTRÉE_UTILISATEUR]" />

Il suffit de fermer l’attribut et l’élément pour injecter du script :

?redirect='/><script>/* PoC */</script><!--

Une fois URL-encodé, le lien pointe vers le vrai domaine de l’application → faible suspicion. À l’ouverture, le script s’exécute : lecture du cookie de session, puis redirection vers un faux formulaire de connexion pour récolter les credentials.

Payload injecté via le paramètre de redirection de la page de login. 1: Le paramètre de redirection réinjecté sans encodage exécute le script (lecture du cookie de session).

Le script s’exécute dans le contexte de l’application. 2: Exécution JavaScript dans l’origine de confiance.

Redirection vers une page contrôlée par l’attaquant. 3: Enchaînement vers une page externe (récolte de credentials).

Validation technique sécurisée

La réflexion peut être contrôlée avec un marqueur inerte sur un compte de test :

curl --fail-with-body --max-time 5 \
  --get --data-urlencode 'redirect=PORTFOLIO_TEST_MARKER' \
  'https://AUTHORIZED_HOST/login'

La correction se vérifie dans le HTML et dans le DOM : la valeur doit rester du texte encodé, jamais devenir un attribut ou un nœud actif. Le principe côté serveur reste l’encodage par contexte et la validation des URL de redirection.

from urllib.parse import urlparse

def local_redirect(value: str) -> str:
    parsed = urlparse(value)
    if parsed.scheme or parsed.netloc or not value.startswith("/"):
        raise ValueError("external redirect rejected")
    return value

La faille

XSS reflété (CWE-79) : entrée non encodée en sortie. Aggravée par l’absence de token CSRF sur le formulaire (CWE-352) et de X-Frame-Options (clickjacking, CWE-1021).

Impact

CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:N = 8.7 (High).

Remédiation

CWE-79 · CWE-601 · CWE-352.