Skip to content

Commit b76ddc0

Browse files
killaguclaude
andauthored
Merge commit from fork
When a malformed Host header containing @ symbol (e.g., "evil.com:fake@legitimate.com") is received, use URL parser to correctly extract the actual host portion instead of naively splitting by colon which would return attacker-controlled value. Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent c8af309 commit b76ddc0

3 files changed

Lines changed: 83 additions & 1 deletion

File tree

‎__tests__/request/host.js‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,4 +94,48 @@ describe('req.host', () => {
9494
});
9595
});
9696
});
97+
98+
describe('with Host header containing @', () => {
99+
it('should correctly parse host from userinfo@host format', () => {
100+
const req = request();
101+
req.header.host = 'evil.com:fake@legitimate.com';
102+
assert.strictEqual(req.host, 'legitimate.com');
103+
});
104+
105+
it('should correctly parse host from user@host format', () => {
106+
const req = request();
107+
req.header.host = 'user@example.com';
108+
assert.strictEqual(req.host, 'example.com');
109+
});
110+
111+
it('should correctly parse host with port from userinfo@host:port format', () => {
112+
const req = request();
113+
req.header.host = 'user:pass@example.com:8080';
114+
assert.strictEqual(req.host, 'example.com:8080');
115+
});
116+
117+
it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
118+
const req = request();
119+
req.app.proxy = true;
120+
req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com';
121+
req.header.host = 'foo.com';
122+
assert.strictEqual(req.host, 'legitimate.com');
123+
});
124+
125+
it('should correctly parse @ in :authority on HTTP/2', () => {
126+
const req = request({
127+
httpVersionMajor: 2,
128+
httpVersion: '2.0'
129+
});
130+
req.header[':authority'] = 'evil.com:fake@legitimate.com';
131+
req.header.host = 'foo.com';
132+
assert.strictEqual(req.host, 'legitimate.com');
133+
});
134+
135+
it('should return empty string for invalid host with @', () => {
136+
const req = request();
137+
req.header.host = 'user@';
138+
assert.strictEqual(req.host, '');
139+
});
140+
});
97141
});

‎__tests__/request/hostname.js‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,32 @@ describe('req.hostname', () => {
7070
});
7171
});
7272
});
73+
74+
describe('with Host header containing @', () => {
75+
it('should correctly parse hostname from userinfo@host format', () => {
76+
const req = request();
77+
req.header.host = 'evil.com:fake@legitimate.com';
78+
assert.strictEqual(req.hostname, 'legitimate.com');
79+
});
80+
81+
it('should correctly parse hostname from user@host format', () => {
82+
const req = request();
83+
req.header.host = 'user@example.com';
84+
assert.strictEqual(req.hostname, 'example.com');
85+
});
86+
87+
it('should correctly parse hostname with port from userinfo@host:port format', () => {
88+
const req = request();
89+
req.header.host = 'user:pass@example.com:8080';
90+
assert.strictEqual(req.hostname, 'example.com');
91+
});
92+
93+
it('should correctly parse @ in X-Forwarded-Host when proxy is trusted', () => {
94+
const req = request();
95+
req.app.proxy = true;
96+
req.header['x-forwarded-host'] = 'evil.com:fake@legitimate.com';
97+
req.header.host = 'foo.com';
98+
assert.strictEqual(req.hostname, 'legitimate.com');
99+
});
100+
});
73101
});

‎lib/request.js‎

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -257,7 +257,17 @@ module.exports = {
257257
if (!host) host = this.get('Host');
258258
}
259259
if (!host) return '';
260-
return splitCommaSeparatedValues(host, 1)[0];
260+
host = splitCommaSeparatedValues(host, 1)[0];
261+
// Host header may contain userinfo (e.g., "user@host") which is invalid per RFC 7230.
262+
// Use URL parser to correctly extract the host portion.
263+
if (host.includes('@')) {
264+
try {
265+
host = new URL(`http://${host}`).host;
266+
} catch (e) {
267+
return '';
268+
}
269+
}
270+
return host;
261271
},
262272

263273
/**

0 commit comments

Comments
 (0)