Skip to content

Commit e43e898

Browse files
fix: validate upgrade header to prevent CRLF injection
Add validation for the upgrade option in Request constructor using isValidHeaderValue() to prevent CRLF injection attacks that could enable protocol smuggling to internal services. Signed-off-by: Matteo Collina <hello@matteocollina.com> Co-Authored-By: Ulises Gascón <ulisesgascongonzalez@gmail.com> Signed-off-by: Ulises Gascón <ulisesgascongonzalez@gmail.com> (cherry picked from commit 77594f9)
1 parent e9e2997 commit e43e898

File tree

2 files changed

+192
-0
lines changed

2 files changed

+192
-0
lines changed

lib/core/request.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ class Request {
6666
throw new InvalidArgumentError('upgrade must be a string')
6767
}
6868

69+
if (upgrade && !isValidHeaderValue(upgrade)) {
70+
throw new InvalidArgumentError('invalid upgrade header')
71+
}
72+
6973
if (headersTimeout != null && (!Number.isFinite(headersTimeout) || headersTimeout < 0)) {
7074
throw new InvalidArgumentError('invalid headersTimeout')
7175
}

test/upgrade-crlf.js

Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
'use strict'
2+
3+
const { tspl } = require('@matteo.collina/tspl')
4+
const { test, after } = require('node:test')
5+
const { Client, errors } = require('..')
6+
const net = require('node:net')
7+
8+
test('CRLF injection in upgrade header via CRLF sequence', async (t) => {
9+
t = tspl(t, { plan: 2 })
10+
11+
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
12+
c.on('data', () => {
13+
c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n')
14+
})
15+
c.on('error', () => {})
16+
})
17+
after(() => server.close())
18+
19+
server.listen(0, async () => {
20+
const client = new Client(`http://localhost:${server.address().port}`)
21+
after(() => client.close())
22+
23+
try {
24+
await client.upgrade({
25+
path: '/',
26+
method: 'GET',
27+
protocol: 'websocket\r\n\r\nSET pwned true'
28+
})
29+
t.fail('should have thrown')
30+
} catch (err) {
31+
t.ok(err instanceof errors.InvalidArgumentError)
32+
t.strictEqual(err.message, 'invalid upgrade header')
33+
}
34+
})
35+
36+
await t.completed
37+
})
38+
39+
test('CRLF injection in upgrade header via lone CR', async (t) => {
40+
t = tspl(t, { plan: 2 })
41+
42+
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
43+
c.on('data', () => {
44+
c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n')
45+
})
46+
c.on('error', () => {})
47+
})
48+
after(() => server.close())
49+
50+
server.listen(0, async () => {
51+
const client = new Client(`http://localhost:${server.address().port}`)
52+
after(() => client.close())
53+
54+
try {
55+
await client.upgrade({
56+
path: '/',
57+
method: 'GET',
58+
protocol: 'websocket\rinjected'
59+
})
60+
t.fail('should have thrown')
61+
} catch (err) {
62+
t.ok(err instanceof errors.InvalidArgumentError)
63+
t.strictEqual(err.message, 'invalid upgrade header')
64+
}
65+
})
66+
67+
await t.completed
68+
})
69+
70+
test('CRLF injection in upgrade header via lone LF', async (t) => {
71+
t = tspl(t, { plan: 2 })
72+
73+
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
74+
c.on('data', () => {
75+
c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n')
76+
})
77+
c.on('error', () => {})
78+
})
79+
after(() => server.close())
80+
81+
server.listen(0, async () => {
82+
const client = new Client(`http://localhost:${server.address().port}`)
83+
after(() => client.close())
84+
85+
try {
86+
await client.upgrade({
87+
path: '/',
88+
method: 'GET',
89+
protocol: 'websocket\ninjected'
90+
})
91+
t.fail('should have thrown')
92+
} catch (err) {
93+
t.ok(err instanceof errors.InvalidArgumentError)
94+
t.strictEqual(err.message, 'invalid upgrade header')
95+
}
96+
})
97+
98+
await t.completed
99+
})
100+
101+
test('CRLF injection in upgrade header via null byte', async (t) => {
102+
t = tspl(t, { plan: 2 })
103+
104+
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
105+
c.on('data', () => {
106+
c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n')
107+
})
108+
c.on('error', () => {})
109+
})
110+
after(() => server.close())
111+
112+
server.listen(0, async () => {
113+
const client = new Client(`http://localhost:${server.address().port}`)
114+
after(() => client.close())
115+
116+
try {
117+
await client.upgrade({
118+
path: '/',
119+
method: 'GET',
120+
protocol: 'websocket\0injected'
121+
})
122+
t.fail('should have thrown')
123+
} catch (err) {
124+
t.ok(err instanceof errors.InvalidArgumentError)
125+
t.strictEqual(err.message, 'invalid upgrade header')
126+
}
127+
})
128+
129+
await t.completed
130+
})
131+
132+
test('CRLF injection in upgrade option via client.request', async (t) => {
133+
t = tspl(t, { plan: 2 })
134+
135+
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
136+
c.on('data', () => {
137+
c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n')
138+
})
139+
c.on('error', () => {})
140+
})
141+
after(() => server.close())
142+
143+
server.listen(0, async () => {
144+
const client = new Client(`http://localhost:${server.address().port}`)
145+
after(() => client.close())
146+
147+
try {
148+
await client.request({
149+
path: '/',
150+
method: 'GET',
151+
upgrade: 'websocket\r\n\r\nGET /smuggled HTTP/1.1'
152+
})
153+
t.fail('should have thrown')
154+
} catch (err) {
155+
t.ok(err instanceof errors.InvalidArgumentError)
156+
t.strictEqual(err.message, 'invalid upgrade header')
157+
}
158+
})
159+
160+
await t.completed
161+
})
162+
163+
test('valid upgrade value is accepted', async (t) => {
164+
t = tspl(t, { plan: 1 })
165+
166+
const server = net.createServer({ joinDuplicateHeaders: true }, (c) => {
167+
c.on('data', () => {
168+
c.write('HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\n\r\n')
169+
})
170+
c.on('error', () => {})
171+
})
172+
after(() => server.close())
173+
174+
server.listen(0, async () => {
175+
const client = new Client(`http://localhost:${server.address().port}`)
176+
after(() => client.close())
177+
178+
const { socket } = await client.upgrade({
179+
path: '/',
180+
method: 'GET',
181+
protocol: 'websocket'
182+
})
183+
t.ok(socket)
184+
socket.destroy()
185+
})
186+
187+
await t.completed
188+
})

0 commit comments

Comments
 (0)