Skip to content

Commit 619c6bc

Browse files
committed
fix(@angular/ssr): prevent malicious URL from overriding host
A request with a specially crafted URL starting with a double slash (e.g., `//example.com`) could cause the server-side rendering logic to interpret the request as being for a different host. This is due to the behavior of the `URL` constructor when a protocol-relative URL is passed as the first argument. This vulnerability could be exploited to make the server execute requests to a malicious domain when relative paths are used within the application (e.g., via `HttpClient`), potentially leading to content injection or other security risks. The fix ensures that the request URL is always constructed as a full URL string, including the protocol and host, before being passed to the `URL` constructor. This prevents the host from being overridden by the path. Closes #31464
1 parent 92ddc42 commit 619c6bc

File tree

2 files changed

+170
-2
lines changed

2 files changed

+170
-2
lines changed

packages/angular/ssr/node/src/request.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,7 @@ function createRequestHeaders(nodeHeaders: IncomingHttpHeaders): Headers {
7676
* @param nodeRequest - The Node.js `IncomingMessage` or `Http2ServerRequest` object to extract URL information from.
7777
* @returns A `URL` object representing the request URL.
7878
*/
79-
function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL {
79+
export function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): URL {
8080
const {
8181
headers,
8282
socket,
@@ -101,7 +101,7 @@ function createRequestUrl(nodeRequest: IncomingMessage | Http2ServerRequest): UR
101101
}
102102
}
103103

104-
return new URL(originalUrl ?? url, `${protocol}://${hostnameWithPort}`);
104+
return new URL(`${protocol}://${hostnameWithPort}${originalUrl ?? url}`);
105105
}
106106

107107
/**
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import { IncomingMessage } from 'node:http';
10+
import { Http2ServerRequest } from 'node:http2';
11+
import { Socket } from 'node:net';
12+
import { createRequestUrl } from '../src/request';
13+
14+
// Helper to create a mock request object for testing.
15+
function createRequest(details: {
16+
headers: Record<string, string | string[] | undefined>;
17+
encryptedSocket?: boolean;
18+
url?: string;
19+
originalUrl?: string;
20+
}): IncomingMessage {
21+
return {
22+
headers: details.headers,
23+
socket: details.encryptedSocket ? ({ encrypted: true } as unknown as Socket) : new Socket(),
24+
url: details.url,
25+
originalUrl: details.originalUrl,
26+
} as unknown as IncomingMessage;
27+
}
28+
29+
// Helper to create a mock Http2ServerRequest object for testing.
30+
function createHttp2Request(details: {
31+
headers: Record<string, string | string[] | undefined>;
32+
url?: string;
33+
}): Http2ServerRequest {
34+
return {
35+
headers: details.headers,
36+
socket: new Socket(),
37+
url: details.url,
38+
} as Http2ServerRequest;
39+
}
40+
41+
describe('createRequestUrl', () => {
42+
it('should create a http URL with hostname and port from the host header', () => {
43+
const url = createRequestUrl(
44+
createRequest({
45+
headers: { host: 'localhost:8080' },
46+
url: '/test',
47+
}),
48+
);
49+
expect(url.href).toBe('http://localhost:8080/test');
50+
});
51+
52+
it('should create a https URL when the socket is encrypted', () => {
53+
const url = createRequestUrl(
54+
createRequest({
55+
headers: { host: 'example.com' },
56+
encryptedSocket: true,
57+
url: '/test',
58+
}),
59+
);
60+
expect(url.href).toBe('https://example.com/test');
61+
});
62+
63+
it('should use "/" as the path when the URL path is empty', () => {
64+
const url = createRequestUrl(
65+
createRequest({
66+
headers: { host: 'example.com' },
67+
encryptedSocket: true,
68+
url: '',
69+
}),
70+
);
71+
expect(url.href).toBe('https://example.com/');
72+
});
73+
74+
it('should preserve query parameters in the URL path', () => {
75+
const url = createRequestUrl(
76+
createRequest({
77+
headers: { host: 'example.com' },
78+
encryptedSocket: true,
79+
url: '/test?a=1',
80+
}),
81+
);
82+
expect(url.href).toBe('https://example.com/test?a=1');
83+
});
84+
85+
it('should prioritize "originalUrl" over "url" for the path', () => {
86+
const url = createRequestUrl(
87+
createRequest({
88+
headers: { host: 'example.com' },
89+
encryptedSocket: true,
90+
url: '/test',
91+
originalUrl: '/original',
92+
}),
93+
);
94+
expect(url.href).toBe('https://example.com/original');
95+
});
96+
97+
it('should use "/" as the path when both "url" and "originalUrl" are not provided', () => {
98+
const url = createRequestUrl(
99+
createRequest({
100+
headers: { host: 'example.com' },
101+
encryptedSocket: true,
102+
url: undefined,
103+
originalUrl: undefined,
104+
}),
105+
);
106+
expect(url.href).toBe('https://example.com/');
107+
});
108+
109+
it('should treat a protocol-relative value in "url" as part of the path', () => {
110+
const url = createRequestUrl(
111+
createRequest({
112+
headers: { host: 'localhost:8080' },
113+
url: '//example.com/test',
114+
}),
115+
);
116+
expect(url.href).toBe('http://localhost:8080//example.com/test');
117+
});
118+
119+
it('should treat a protocol-relative value in "originalUrl" as part of the path', () => {
120+
const url = createRequestUrl(
121+
createRequest({
122+
headers: { host: 'localhost:8080' },
123+
url: '/test',
124+
originalUrl: '//example.com/original',
125+
}),
126+
);
127+
expect(url.href).toBe('http://localhost:8080//example.com/original');
128+
});
129+
130+
it('should normalize a path starting with three slashes to two slashes', () => {
131+
const url = createRequestUrl(
132+
createRequest({
133+
headers: { host: 'localhost:8080' },
134+
url: '///example.com/test',
135+
}),
136+
);
137+
expect(url.href).toBe('http://localhost:8080//example.com/test');
138+
});
139+
140+
it('should prioritize "x-forwarded-host" and "x-forwarded-proto" headers', () => {
141+
const url = createRequestUrl(
142+
createRequest({
143+
headers: {
144+
host: 'localhost:8080',
145+
'x-forwarded-host': 'example.com',
146+
'x-forwarded-proto': 'https',
147+
},
148+
url: '/test',
149+
}),
150+
);
151+
expect(url.href).toBe('https://example.com/test');
152+
});
153+
154+
it('should use "x-forwarded-port" header for the port', () => {
155+
const url = createRequestUrl(
156+
createRequest({
157+
headers: {
158+
host: 'localhost:8080',
159+
'x-forwarded-host': 'example.com',
160+
'x-forwarded-proto': 'https',
161+
'x-forwarded-port': '8443',
162+
},
163+
url: '/test',
164+
}),
165+
);
166+
expect(url.href).toBe('https://example.com:8443/test');
167+
});
168+
});

0 commit comments

Comments
 (0)