Skip to content

Commit 942d76f

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 942d76f

File tree

2 files changed

+160
-2
lines changed

2 files changed

+160
-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: 158 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,158 @@
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 prioritize "x-forwarded-host" and "x-forwarded-proto" headers', () => {
131+
const url = createRequestUrl(
132+
createRequest({
133+
headers: {
134+
host: 'localhost:8080',
135+
'x-forwarded-host': 'example.com',
136+
'x-forwarded-proto': 'https',
137+
},
138+
url: '/test',
139+
}),
140+
);
141+
expect(url.href).toBe('https://example.com/test');
142+
});
143+
144+
it('should use "x-forwarded-port" header for the port', () => {
145+
const url = createRequestUrl(
146+
createRequest({
147+
headers: {
148+
host: 'localhost:8080',
149+
'x-forwarded-host': 'example.com',
150+
'x-forwarded-proto': 'https',
151+
'x-forwarded-port': '8443',
152+
},
153+
url: '/test',
154+
}),
155+
);
156+
expect(url.href).toBe('https://example.com:8443/test');
157+
});
158+
});

0 commit comments

Comments
 (0)