June 6, 2026
Angular SSR Open Redirect Vulnerabilities Explained (CVE-2026-27738, CVE-2026-33397)
Analysis of Angular SSR open redirect vulnerabilities caused by X-Forwarded-Prefix handling flaws and URL normalization issues.
HCN
3 min read
Introduction
In this year(2026) start , 3 security advisories was published for Angular SSR regarding an open redirect vulnerabilities. Here are CVEs
- CVE-2026–27738 | Feb 23, 2026 | reporter — VenkatKwest
- CVE-2026–33397 | Mar 19, 2026 | reporter — VenkatKwest
- CVE-2026–44437 | May 03, 2026 | reporter — kimkou2024
First 2 CVEs the same cause and second one is bypassing the earlier introduce validation mechanism. In this article we talk about the first 2 CVEs.
Before understanding how this vulnerability occurs, it is necessary to understand X-Forwarded-Prefix and protocol-relative requests.
Why X-Forwarded-Prefix
Sometimes you need to host multiple web applications on the same domain at different paths. For example, the main marketing pages at /, /contact, and /about serve one application, while /shop serves a different application, with /shop as its entry point.
In this scenario, a proxy can be configured to route all /shop/* requests to the second application after stripping the /shop prefix. As a result, the /shop application receives /shop as /, and /shop/cart as /cart.
Now consider an endpoint /shop/oldpage that should redirect to /shop/newpage. The server must return a redirect response with a Location header pointing to /shop/newpage. However, the application only sees the path as /oldpage, so it may generate a redirect to /newpage, which is incorrect.
In this setup, the proxy needs to be configured to include the X-Forwarded-Prefix header. This allows Angular to detect the original /shop prefix and correctly construct the Location header for the redirect response.
The problem is that an attacker can send a request with a malicious X-Forwarded-Prefix. Angular uses this value to define the Location header. For example, with X-Forwarded-Prefix: /shop, Angular sets the Location header value to /shop/newpage.
Protocol-relative request
When a redirect response contains //shop/newpage instead of a single leading slash, browsers interpret this as a protocol-relative URL, where the first path segment is treated as the hostname. As a result, the browser redirects to https://shop/newpage, leading to an open redirect.
X-Forwarded-Prefix: //shop does not directly generate //shop/newpage as the Location header value, but in some cases, double leading slashes in the resulting path cause the browser to interpret it as a protocol-relative request.
CVE-2026–27738
Double (or triple) leading slashes
To understand why //shop/newpage is not produced for X-Forwarded-Prefix: //shop, consider the joinUrlParts() function in packages/angular/ssr/src/utils/url.ts, which is used to join the X-Forwarded-Prefix value.
export function joinUrlParts(…parts: string[]): string {
const normalizeParts: string[] = [];
for (const part of parts) {
if (part === '') {
// Skip any empty parts
continue;
}
let normalizedPart = part;
if (part[0] === '/') {
normalizedPart = normalizedPart.slice(1);
}
if (part.at(-1) === '/') {
normalizedPart = normalizedPart.slice(0, -1);
}
if (normalizedPart !== '') {
normalizeParts.push(normalizedPart);
}
}
return addLeadingSlash(normalizeParts.join('/'));
}export function joinUrlParts(…parts: string[]): string {
const normalizeParts: string[] = [];
for (const part of parts) {
if (part === '') {
// Skip any empty parts
continue;
}
let normalizedPart = part;
if (part[0] === '/') {
normalizedPart = normalizedPart.slice(1);
}
if (part.at(-1) === '/') {
normalizedPart = normalizedPart.slice(0, -1);
}
if (normalizedPart !== '') {
normalizeParts.push(normalizedPart);
}
}
return addLeadingSlash(normalizeParts.join('/'));
}In joinUrlParts, each part is normalized by removing a single leading and trailing / without additional validation.
When the X-Forwarded-Prefix header contains multiple leading slashes, such as ///evil.com, only one leading slash is removed during normalization, with no further sanitization or validation applied.
Impact
Therefore, by using X-Forwarded-Prefix: ///evil.com, an attacker can perform an open redirect. It becomes more serious when the cache does not vary on the X-Forwarded-Prefix header. An attacker can poison cached redirect responses, causing users to be redirected to an attacker-controlled website.
Fix
Angular maintainers fixed the issue by:
- Updating
joinUrlPartsto strip all leading and trailing slashes from URL segments. - Adding strict validation for the
X-Forwarded-Prefixheader using a regex.
CVE-2026–33397
As discussed earlier, protocol-relative requests are not limited to double leading slashes. Modern browsers may interpret the /\ sequence as //. If the Location header is /\evil.com/newpage, it can still trigger an open redirect and the same cache poisoning issue.
However, X-Forwarded-Prefix: /\evil.com does not work directly due to validation of the X-Forwarded-Prefix header
const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\\.\\.?(?:[/\\]|$)/;const INVALID_PREFIX_REGEX = /^[/\\]{2}|(?:^|[/\\])\\.\\.?(?:[/\\]|$)/;By looking at the updated joinUrlParts:
export function joinUrlParts(…parts: string[]): string {
const normalizedParts: string[] = [];
for (const part of parts) {
if (part === '') {
continue;
}
let start = 0;
let end = part.length;
// Use pointers to avoid intermediate slices
while (start < end && part[start] === '/') {
start++;
}
while (end > start && part[end - 1] === '/') {
end - ;
}
if (start < end) {
normalizedParts.push(part.slice(start, end));
}
}
return addLeadingSlash(normalizedParts.join('/'));
}export function joinUrlParts(…parts: string[]): string {
const normalizedParts: string[] = [];
for (const part of parts) {
if (part === '') {
continue;
}
let start = 0;
let end = part.length;
// Use pointers to avoid intermediate slices
while (start < end && part[start] === '/') {
start++;
}
while (end > start && part[end - 1] === '/') {
end - ;
}
if (start < end) {
normalizedParts.push(part.slice(start, end));
}
}
return addLeadingSlash(normalizedParts.join('/'));
}After normalization, a leading / is added. This means only \evil.com is needed as the value, and it is not caught by the regex.
As a result, using a value like \evil.com, an attacker can trigger an open redirect again.