Aller au contenu principal
Retour au blog

Symfony 8.2 : URLs signées à usage unique avec UriSigner

Symfony 8.2 ajoute $version à UriSigner : liens de reset invalidés après usage, expiration obligatoire en 9.0 et paramètres signés dans les login links.

Flavien Métivier · 10 min de lecture
Sommaire · 8 parties

Une URL signée est un lien dont la signature empêche toute modification. Avec UriSigner, Symfony 8.2 permet aussi de la rendre invalide après son premier usage, rend l'expiration obligatoire à terme, et signe enfin les paramètres des login links. Présentés le 9 octobre 2026 sur le blog officiel, ces changements de Kevin Bond (PR #64408 et #64455) et de Jan Klan (PR #64271) arrivent avec Symfony 8.2, attendu en novembre 2026.

Un lien de réinitialisation de mot de passe a une mission simple : servir une fois, puis mourir. Avec UriSigner, il faisait la première moitié du travail. La signature empêchait de le falsifier, pas de le rejouer : tant que l'expiration n'était pas atteinte, le lien restait valide, quel que soit le nombre de clics. Tu as donc déjà écrit la rustine, ou croisé quelqu'un qui l'a écrite : une table de jetons, une colonne used_at, un job de purge.

Pourquoi une URL signée reste valable après usage

Une signature prouve que l'URL n'a pas été modifiée depuis sa génération. Elle ne dit rien du nombre d'utilisations. Un lien de reset, de confirmation d'e-mail ou de téléchargement signé reste donc valide jusqu'à son expiration. Jusqu'ici, il fallait écrire sa propre logique d'invalidation, en général un jeton stocké en base. C'est cette plomberie que Symfony 8.2 permet de supprimer dans les cas les plus courants.

Comment rendre une URL signée à usage unique avec $version

Les méthodes sign(), check(), checkRequest() et verify() d'UriSigner acceptent un nouvel argument $version. Le paramètre $version est ajouté à la signature mais pas à l'URL : le lien reste valide tant que sa valeur ne change pas. Pour un reset de mot de passe, le hash du mot de passe est le candidat évident. Exemple du billet :

php
use Symfony\Component\HttpFoundation\UriSigner;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;

// when sending the password reset email
$url = $uriSigner->sign(
    $this->generateUrl('reset_password', ['id' => $user->getId()], UrlGeneratorInterface::ABSOLUTE_URL),
    new \DateInterval('PT30M'),
    $user->getPassword(),
);

// in the controller that handles the link
$uriSigner->verify($request, $user->getPassword());

Un détail de comportement compte ici : verify() n'invalide rien. Le lien devient caduc quand ton application enregistre le nouveau hash. Après cela, tout appel à verify() avec le hash courant lève une UnverifiedSignedUriException, parce que la signature a été calculée avec l'ancien. D'après la PR, la version entre dans le calcul sous un nom de paramètre réservé, _version, qui n'apparaît jamais dans l'URL finale.

Quelle valeur choisir pour $version selon le type de lien ?

« Usage unique » est un raccourci. Il n'y a ni compteur ni jeton consommé : c'est une invalidation par changement d'état, sans rien à stocker. Le lien n'est à usage unique que si l'action qu'il déclenche modifie bien la valeur de version. Le choix de cette valeur est donc la vraie décision d'architecture :

  • Reset de mot de passe : le hash du mot de passe, exemple du billet.
  • Login link : la date de dernière connexion, citée par le billet.
  • Confirmation d’e-mail : la date de vérification de l’adresse, citée par le billet.
  • Téléchargement signé : il faut une valeur d’état qui change à la révocation (version du fichier, horodatage de révocation). C’est ma recommandation, pas un cas documenté ; sans état qui bouge, le lien reste valable jusqu’à son expiration.

Si l'action déclenchée par ton lien ne change aucun état signable, ce mécanisme ne te protège pas, et un jeton révocable stocké en base reste la bonne réponse. Autre point, qui relève de mon analyse : l'invalidation n'arrive qu'à l'enregistrement du nouvel état. Deux requêtes concurrentes avec le même lien peuvent passer verify() avant le premier commit. Pour un reset de mot de passe, enchaîne vérification et écriture dans une transaction avec verrou sur l'utilisateur.

Expiration obligatoire : quoi changer avant Symfony 9.0

Un lien signé sans expiration est valable pour toujours : celui qui traîne dans un vieil e-mail ou une ligne de log reste exploitable des années plus tard. Depuis Symfony 8.2, appeler UriSigner::sign() sans expiration est déprécié ; en Symfony 9.0, l'expiration sera obligatoire. Tu peux la passer à chaque appel, ou fixer un défaut via le nouvel argument de constructeur $defaultExpiration (un \DateInterval ou un nombre de secondes). Avec le service uri_signer du framework, une option suffit :

yaml
# config/packages/framework.yaml
framework:
    uri_signer:
        expiration: 3600

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.

Calculer mon audit →

La dépréciation ne se déclenche que si aucune expiration n'est passée ni configurée. Fixer un défaut global règle le bruit d'un coup, mais une heure convient mal à un lien de téléchargement partagé et très bien à un reset : tranche cas d'usage par cas d'usage plutôt que de poser 3 600 secondes partout. Note enfin que ajouter $version à une vérification invalide les liens déjà émis sans version : déploie sign() et verify() ensemble, et laisse expirer les anciens liens.

Un login link contient l'identifiant de l'utilisateur, la date d'expiration et une signature. Si tu y ajoutais d'autres paramètres, par exemple la page où rediriger après connexion, rien ne les protégeait. createLoginLink() accepte désormais un argument $parameters : ils sont ajoutés à l'URL et couverts par la signature.

php
$loginLinkDetails = $loginLinkHandler->createLoginLink($user, parameters: [
    'return_to' => '/account/settings',
]);

Modifier ou retirer l'un de ces paramètres invalide le lien. À l'usage, Symfony place les valeurs vérifiées dans l'attribut de requête _login_link_parameters :

php
$returnTo = $request->attributes->get('_login_link_parameters')['return_to'] ?? null;

Selon la PR, le lien porte en plus un paramètre hash_parameters qui liste les noms couverts par la signature, avec quelques contraintes : noms limités à [A-Za-z0-9_.-], noms réservés (user, expires, hash), valeurs scalaires ou Stringable. Le login link est aussi une méthode d'authentification parmi d'autres, ce que Symfony 8.2 sait désormais tracer : voir notre article sur les preuves de méthode d'authentification.

Piège : ne lis jamais return_to dans la query string

Lis toujours return_to depuis _login_link_parameters, jamais depuis la query string. Symfony ignore volontairement les paramètres non signés, comme ceux de tracking que les clients mail ajoutent aux liens ; n'importe qui peut donc en ajouter. Un $request->query->get('return_to') par réflexe contourne la protection que tu viens d'activer, et rouvre la redirection arbitraire que la signature devait fermer. La PR signale le même risque avec _target_path, lu dans la requête brute par le handler de succès par défaut. Notre article sur les redirections, le logout et le CSRF dans Symfony détaille ce type de faille.

Checklist de migration vers Symfony 8.2

  • Lance un grep sur ->sign( et liste les appels sans expiration.
  • Fixe framework.uri_signer.expiration à une valeur conservatrice, puis surcharge par cas d’usage.
  • Remplace tes tables de jetons de reset par $version là où une valeur d’état change réellement à l’usage, et teste le scénario « lien rejoué après changement ».
  • Passe tes redirections de login links dans $parameters et lis-les uniquement via _login_link_parameters.
  • Si tu as une implémentation maison de LoginLinkHandlerInterface, adapte sa signature.

Ma lecture

L'expiration obligatoire est le sujet à traiter maintenant : elle ne coûte presque rien et supprime une classe entière de liens éternels. $version vaut le détour, avec une réserve : ne l'adopte que si tu peux nommer la valeur qui change quand le lien a servi. Si tu hésites, ton lien n'est pas à usage unique, et un jeton révocable reste plus honnête. Quant aux paramètres signés des login links, c'est un correctif de sécurité déguisé en fonctionnalité.

Tu n'as pas besoin d'attendre Symfony 8.2 pour savoir combien de liens signés sans expiration ou de redirections non vérifiées dorment dans ton application. Un Bear Scan le montre dès aujourd'hui : en dix jours, un état des lieux de ta base de code, sécurité applicative comprise, avec une liste d'actions classées par priorité.

Cet article vous a été utile ? Partagez-le.

Portrait de Flavien Métivier, fondateur de The Bearded Bear : barbe longue, lunettes, sur fond sombre

Écrit par

Flavien Métivier

Expert PHP/Symfony indépendant depuis 2014, ancien CTO de Kartable et d'Extrabat. Plus de 80 projets menés, de l'audit à la migration.

Mon parcours

À lire ensuite

Votre dette technique s'accumule ?

Audit complet en 10 jours. Recommandations priorisées et actionnables.

Newsletter Bear Bytes

Veille Claude Code, Symfony et PHP, retours d'expérience terrain et conseils techniques, directement dans votre boîte mail.