Feb 26, 2026·7 min read·52 visits
Angular SSR's URL normalization logic only stripped a single leading slash from path segments. Attackers sending a `X-Forwarded-Prefix: ///evil.com` header can trick the server into generating a `Location: //evil.com` redirect. This protocol-relative URL forces the browser to navigate to the attacker's domain, enabling high-credibility phishing attacks.
A deceptively simple logic error in Angular's Server-Side Rendering (SSR) engine allows attackers to turn internal redirects into open redirects. By exploiting how the framework normalizes URLs from the `X-Forwarded-Prefix` header, a malicious actor can bypass validation with extra slashes, leading to protocol-relative URL redirection. This flaw affects major versions 19, 20, and 21, turning trusted applications into phishing launchpads.
Server-Side Rendering (SSR) is the magic glue that makes heavy JavaScript frameworks feel snappy and SEO-friendly. But SSR is also a dangerous bridge between the raw, untrusted internet and your internal application logic. In the modern cloud stack, your Angular app rarely sits naked on the internet. It lives behind layers of Nginx, AWS ALBs, or Cloudflare, all of which helpfully annotate requests with headers like X-Forwarded-For and X-Forwarded-Prefix so the app knows where it supposedly lives.
Here is the problem: Angular decided to trust these headers a little too much. Specifically, the @angular/ssr package needs to construct full URLs when performing redirects (like when you hit a protected route and get bounced to /login). To do this, it stitches together the base URL and the target path. It sounds simple. It sounds boring. But in the world of exploit development, "boring" utility functions are where the bodies are buried.
CVE-2026-27738 isn't a complex buffer overflow or a heap grooming masterpiece. It is a testament to the fact that developers still treat input validation like a chore rather than a survival skill. The vulnerability lies in a helper function designed to clean up URL paths. It tries to be helpful by removing leading slashes to avoid double-slashing. Spoiler alert: it didn't remove enough of them.
Let's talk about joinUrlParts. This utility function in packages/angular/ssr/src/utils/url.ts has one job: take a bunch of string segments and glue them together into a valid URL path. To prevent the classic //path//subpath mess, it attempts to normalize the segments by stripping leading slashes before joining them.
Here is where the developer's optimism collided with reality. The code checked if a segment started with a slash. If it did, it removed it. Singular. Once. It assumed that a path segment would only ever have one accidental leading slash. It looked something like this:
// The logic of a optimist
if (part[0] === '/') {
normalizedPart = normalizedPart.slice(1);
}See the issue? If I hand you /home, you give me home. Perfect. But what if I hand you ///evil.com? The code sees the first slash, says "Gotcha!", removes it, and leaves //evil.com. In the world of browsers, // isn't just two slashes; it's a protocol-relative URL. It tells the browser, "Keep using HTTPS, but switch the host to evil.com."
This is the digital equivalent of a bouncer checking your ID, seeing you have a fake mustache, peeling it off, but failing to notice the second, smaller fake mustache underneath.
The fix provided by the Angular team is a textbook example of "defense in depth" replacing "naive trust." They didn't just fix the loop; they scorched the earth. Let's look at the critical diff in the joinUrlParts function.
The Vulnerable Code:
// Old logic: strictly checks index 0
if (part.startsWith('/')) {
part = part.substring(1);
}The Fix (Commit 877f017):
The new implementation treats slashes like an infestation. It uses a while loop to aggressively strip every leading and trailing slash it finds. It doesn't matter if you send ///, ////, or //////////////////—they are all getting nuked.
export function joinUrlParts(...parts: string[]): string {
const normalizedParts: string[] = [];
for (const part of parts) {
let start = 0;
let end = part.length;
// Eat all the slashes at the start
while (start < end && part[start] === '/') { start++; }
// Eat all the slashes at the end
while (end > start && part[end - 1] === '/') { end--; }
if (start < end) {
normalizedParts.push(part.slice(start, end));
}
}
return addLeadingSlash(normalizedParts.join('/'));
}But they didn't stop there. They realized that X-Forwarded-Prefix shouldn't contain suspicious characters at all. They added a regex validator INVALID_PREFIX_REGEX that instantly throws an error if the header contains double slashes or directory traversal sequences (..).
const INVALID_PREFIX_REGEX = /^[\/\\]{2}|(?:^|[\/\\])\.\.?(?:[\/\\]|$)/;This is the difference between "patching a bug" and "killing a bug class."
So, how do we weaponize this? We need an Angular SSR app sitting behind a proxy (like Nginx) that blindly passes the X-Forwarded-Prefix header. This is a very common configuration for apps hosted on sub-paths (e.g., example.com/app/).
The Setup:
https://vulnerable-bank.comhttps://evil-phishing.com/logout route, which redirects users back to /home after clearing the session.The Attack: We craft a link or use a CSRF chain to force a request with a manipulated header. If we can control the upstream proxy or if the application is misconfigured to trust client headers directly (common in dev/staging), we send this:
GET /logout HTTP/1.1
Host: vulnerable-bank.com
X-Forwarded-Prefix: ///evil-phishing.comThe Execution:
joinUrlParts('///evil-phishing.com', 'home').//evil-phishing.com.//evil-phishing.com/home.HTTP/1.1 302 Found
Location: //evil-phishing.com/homeThe Result:
The victim's browser sees //evil-phishing.com/home. Since the original page was loaded over HTTPS, the browser expands this to https://evil-phishing.com/home. The user is silently whisked away to the attacker's domain, which likely looks exactly like the bank's login page.
Security engineers often roll their eyes at Open Redirects. "So what? The user goes to the wrong page." But in the context of a trusted application, an Open Redirect is a silver bullet for social engineering.
First, there is the Phishing Implication. Users are trained to check the URL bar. If they click a link that starts with vulnerable-bank.com, they let their guard down. If that link immediately 302s them to a clone site, they rarely notice the switch. The initial trust is established by the valid domain.
Second, consider Cache Poisoning. If the X-Forwarded-Prefix header is not part of the cache key (the Vary header) on your CDN, an attacker could send a single malicious request that gets cached. Now, every legitimate user who visits that page gets redirected to the malware site. This turns a targeted attack into a Denial of Service or a mass-compromise event.
Finally, this bypasses many SSRF filters. While this specific CVE results in a client-side redirect, the underlying logic flaw—insufficient sanitization of path components—often mirrors how internal requests are constructed. If this logic was used to fetch internal resources, we'd be looking at a much nastier RCE scenario.
If you are running Angular SSR on versions 19, 20, or 21, you are exposed. The immediate fix is to upgrade to the patched versions:
21.1.520.3.1719.2.21If you cannot upgrade immediately (corporate bureaucracy is the real vulnerability, after all), you must sanitize the input before it reaches Angular. In your server.ts or Nginx configuration, you need to flatten that header.
Nginx Mitigation: Ensure you are hardcoding the prefix if you know it, rather than trusting the client. If you must pass it dynamically, validate it.
# Don't do this if you can avoid it
# proxy_set_header X-Forwarded-Prefix $http_x_forwarded_prefix;
# Do this: explicitly define the prefix
proxy_set_header X-Forwarded-Prefix /my-app;Node.js Middleware Mitigation:
// Add this early in your server stack
app.use((req, res, next) => {
const prefix = req.headers['x-forwarded-prefix'];
if (typeof prefix === 'string' && prefix.startsWith('//')) {
// Flatten multiple slashes to one
req.headers['x-forwarded-prefix'] = prefix.replace(/^\/+/, '/');
}
next();
});This isn't just about fixing a bug; it's about acknowledging that "normalization" is hard and that headers are user input. Treat them accordingly.
CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N| Product | Affected Versions | Fixed Version |
|---|---|---|
@angular/ssr Angular | >= 19.0.0-next.0, < 19.2.21 | 19.2.21 |
@angular/ssr Angular | >= 20.0.0-next.0, < 20.3.17 | 20.3.17 |
@angular/ssr Angular | >= 21.0.0-next.0, < 21.1.5 | 21.1.5 |
| Attribute | Detail |
|---|---|
| CWE | CWE-601 (Open Redirect) |
| CVSS v4.0 | 6.9 (Medium) |
| Attack Vector | Network (Header Manipulation) |
| Exploit Maturity | Proof of Concept |
| Privileges | None |
| User Interaction | Required (Victim must click link) |
URL Redirection to Untrusted Site ('Open Redirect')
An unauthenticated remote denial of service vulnerability exists in the Unleash feature management platform. By submitting a crafted JSON payload containing deeply nested structures to an OpenAPI-validated endpoint, an attacker can trigger uncontrolled recursion within the error formatting module. This leads to a call-stack exhaustion (RangeError: Maximum call stack size exceeded) inside the Node.js runtime, causing the service to crash immediately without recovery.
CVE-2026-63004 is a server-side request forgery (SSRF) vulnerability in the Unleash feature management platform. Authenticated administrators with CREATE_ADDON or UPDATE_ADDON privileges can exploit this vulnerability to initiate requests to loopback addresses, private networks, and cloud metadata endpoints, potentially leading to information disclosure and credential extraction.
Prior to version 8.0.3, Unleash's Markdown event formatter directly mutated the global template-escaping function of the shared mustache Node.js module, resulting in a process-wide security degradation where HTML/Markdown escaping was permanently disabled for the application lifetime.
A critical SQL injection vulnerability exists in the GeoTools open-source Java library. This vulnerability is situated within the post-processing phase of OGC Filter conversion inside the PostGIS DataStore module. Specifically, the `jsonArrayContains` function does not validate or sanitize its arguments before constructing PostgreSQL SQL/JSON path evaluation queries. An unauthenticated remote attacker can exploit this weakness by submitting crafted filters via standard OGC services like WFS or WMS to execute arbitrary SQL commands on the underlying database system.
CVE-2026-61824 is a high-severity Cross-Site Scripting (XSS) vulnerability in kepano/defuddle before version 0.19.1. Custom site extractors for platforms such as X/Twitter, Substack, and YouTube constructed HTML representations via template string interpolation without output escaping. This allowed malicious pages to bypass standard parser sanitization routines and execute arbitrary JavaScript.
A high-severity vulnerability exists in KeystoneJS, a popular Node.js CMS and GraphQL framework, where the query resolution engine fails to validate signed negative integers within the pagination subsystem. Unauthenticated remote attackers can leverage this flaw to bypass the 'graphql.maxTake' safety boundary. By sending large negative values in the 'take' query parameter, the underlying Prisma ORM interprets the value as an instruction to fetch rows from the end of the collection, allowing malicious actors to bypass pagination limits, trigger database resource exhaustion, and execute application-level Denial of Service (DoS) attacks.