The small link after a large security decision

A customer opens a saved order page, is asked to sign in, and reasonably expects to arrive at that order afterward. The common implementation carries a return destination through the login request. That destination is still input: a visitor can change a query parameter, submit a crafted form, or share a link with a plausible business domain at the front. If the server sends the browser straight to whatever address it receives, an attacker can use the familiar login address as a stepping stone to a phishing page. OWASP describes precisely this class of unvalidated redirect. The owner’s decision is therefore not whether every return link is dangerous. It is which destinations the service has a legitimate reason to support, and which component has authority to choose them.

The problem can be missed because the login itself works. A tester who signs in through the ordinary home page may never exercise a malicious or malformed return value; an automated check that sees a successful authentication may also miss the final Location response. Treat the landing step as its own security and usability requirement. Write down the few journeys that need continuity, such as a saved quotation, a booking, or an account setting. Record what should happen when that page no longer exists, belongs to another customer, or requires a different role. A correct destination is not merely on the right host: it must still be appropriate for the authenticated person and the current request.

Choose a destination model before validating strings

The simplest model is often a server-owned destination identifier rather than a URL supplied by the browser. For example, a request may carry a short identifier meaning “my orders,” while the server maps that identifier to its own route after login. A more specific destination, such as an order detail, needs an order identifier that is checked against the signed-in customer before the page is shown. This reduces the number of URL forms the login handler has to understand. OWASP recommends a server-side mapping where possible and warns that identifiers must not become a way to enumerate protected targets. The mapping is a design choice, not a substitute for authorization on the target page.

Sometimes a service genuinely needs a local return path, such as returning to a search result with selected filters. Use the web framework’s maintained local-URL helper when it provides one. Microsoft’s ASP.NET Core guidance, for example, documents IsLocalUrl and LocalRedirect for this purpose. Agree on the accepted shape: perhaps a path beginning with one slash, an approved set of route names, and only expected query keys. Reject a scheme, authority, backslash ambiguity, control characters, and values that the framework interprets as another origin. The exact parser behavior matters more than a handwritten visual rule. When the value fails, send the customer to a safe account landing page and keep authentication successful; do not let a bad return value strand a legitimate user.

Give external journeys a separate decision

An external partner may ask to receive a customer after sign-in, but that is a different requirement from “return to the page the customer requested.” Do not widen a local return rule just because a partner integration needs one destination. Decide whether the customer should instead see a clearly labeled link and choose to leave the site. If an automatic external redirect is truly necessary, OWASP advises an explicit allowlist of approved destinations. Compare parsed scheme, canonical host, effective port, and any required path restriction. A prefix such as a trusted name at the start of a longer hostname is insufficient, as is a raw substring test. Credentials in the URL authority can also make a malicious address appear to contain a trusted name.

A return destination should also be bound to the journey that produced it. Consider a booking customer who starts at one service, opens a different account tab, and completes login later. A stale destination can be technically local yet surprising. Preserve the intended state in a short-lived server-side session or another mechanism appropriate to the application, and decide what happens if it expires. Recheck access after authentication rather than trusting a pre-login assumption. If the target route cannot be reconstructed safely, a stable account page with a clear route to “resume booking” is better than silently sending the person to an unrelated page. These are product rules that engineering and customer support should agree on before implementation.

Walk through an order-page example

Suppose a customer follows a link to an order detail while signed out. The application records the order identifier as part of a server-side pending journey and sends the browser to sign in. After successful authentication, it asks the order service whether this customer may read that order. If yes, it constructs the local route itself and redirects there. If the order was cancelled or is not visible to that account, it returns to “my orders” with a plain explanation. This flow keeps the customer’s original purpose when it is valid, without accepting an arbitrary external address. A team using a local return path instead can produce the same outcome only if parsing, local-destination checks, route scope, and target authorization all pass.

Now test the same flow with a crafted link whose return value points to an unrelated site, a lookalike subdomain, a URL with user information before the host, or a protocol-relative form. The result should be the agreed safe landing page, not an external Location header. Try encoded variants and a valid local path with an unauthorized order identifier. Verify both server response and browser destination; do not judge by the login screen alone. Also test a normal deep link from an email, a bookmarked page, and a return value that has expired. Success means legitimate continuity survives while untrusted destinations cannot override it. Keep the malicious examples in a controlled test environment rather than sending customers test links.

Make the rule maintainable

Record the approved destination model alongside the login flow, not in an undocumented regular expression. Owners of new routes should know whether the route may be a login return target and what happens if access changes. A regression test should cover the allowed route, denied route, malformed URL and external origin every time authentication code or routing changes. Inspect any other path that redirects after password reset, account invitation or email verification, because it may reuse the same destination handling. Review logs for rejected destinations without storing entire sensitive query strings. A spike in rejects deserves investigation, but a single reject can also be a stale bookmark or broken integration; logs are evidence to interpret, not an automatic attack verdict.

The practical acceptance question is simple: can a signed-out customer reach the intended, authorized page after login while a visitor-controlled value cannot send anyone to an unapproved origin? Demonstrate both halves. If the application cannot reliably decide whether a complex return URL is safe, narrow the feature to server-owned destinations and provide a manual link for exceptional external journeys. That choice may add a click, but it makes the product behavior explainable to customers and support staff. For a Hong Kong SME with bilingual journeys, test the same destination behavior from both language entry points; a language switch must not become a second, less-checked redirect mechanism.

Decide what failure looks like to customers

A safe fallback needs customer language as well as technical rejection. If a booking link has expired, the landing page should say that the original destination is unavailable and offer a route to current bookings. Do not echo the rejected address into the page or put it into a clickable “continue” link, because that recreates the attacker’s delivery path. A rejected destination also should not erase a completed login or send the person through authentication repeatedly. These details matter to support staff: they can distinguish a genuinely expired invitation from a sign-in failure without asking customers to forward sensitive full URLs. The fallback copy should be written in both site languages and tested from each route.

When the login path crosses several services, identify the exact component that issues the final redirect. A front-end router may read a return value after the server has already validated a different one; a partner gateway may add a second redirect of its own. Trace a normal journey from the first unauthenticated request through the final browser address, recording which layer stores and transforms the destination. The decisive check belongs immediately before the component emits the redirect, using the value it will actually send. Validation at an earlier step can become stale if a later layer decodes, concatenates or substitutes the string. This is a design review task, not a claim that any particular framework is vulnerable.

For handover, give the team a small decision table: an approved route with access goes to the intended page; an approved route without access goes to the account page; a malformed or external destination goes to the safe fallback; an expired pending journey invites the customer to restart. The table should identify the owner of each result and the message shown. It lets customer support reproduce a case and lets testers assert the observed Location header rather than only that the user eventually sees some page. As the site adds new languages or account roles, the same table provides a controlled way to extend the journey without changing the fundamental trust boundary.

Before you proceed

  • The login flow accepts only documented destination forms.
  • An unauthorized local page and every unapproved external origin fall back safely.
  • A normal deep link works in both language journeys.

Sources & further reading