Symfony 8.2 New Published on

A login link that keeps its destination

createLoginLink() now signs its parameters: the return_to of a login link can no longer be tampered with.

Topics covered: Security

New in Symfony 8.2 (in development). A login link (login_link) logs you in without a password: it carries the identifier, an expiration date and a signature. Anything added to it afterwards — typically the page to bring the reader back to — stayed outside the signature. Anyone could therefore change it, and turn a genuine link, sent by you, into a redirect to another site.

Signed parameters

createLoginLink() now accepts parameters, which the signature covers:

$link = $loginLinks->createLoginLink($user, $request, null, ['return_to' => '/account/invoices']);

Change or remove one of them, and the link no longer logs anyone in. On arrival, the verified parameters wait in a request attribute:

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

Read them there, not in the query string: a parameter added afterwards — an email tracker, or worse — shows up there without having been signed. Symfony ignores it; your code must do the same.

The signature proves that return_to comes from you, not that it leads to you: anyone who can request a link can also request a signed link to another site. So only accept a path on your own site there.

Your task

LienController builds the link that would go out by email (the response shows it) and appends return_to to it by hand; RedirectionApresLien reads it back from the query string. The link is authentic, its destination isn't: replace /account/invoices with another site, and the reader ends up there, logged in.

  1. Have the link sign return_to.
  2. In RedirectionApresLien, read only the signed parameters.
  3. Only follow a path on the site there (/…, but not //…); otherwise, redirect to /account.

Where the feature comes from: the original pull request.