Skip to content

Commit 33928bc

Browse files
committed
fix: validate blob body content type
1 parent 98011a8 commit 33928bc

2 files changed

Lines changed: 42 additions & 2 deletions

File tree

lib/dispatcher/client-h1.js

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ const {
1010
RequestContentLengthMismatchError,
1111
ResponseContentLengthMismatchError,
1212
RequestAbortedError,
13+
InvalidArgumentError,
1314
HeadersTimeoutError,
1415
HeadersOverflowError,
1516
SocketError,
@@ -1134,8 +1135,16 @@ function writeH1 (client, request) {
11341135
}
11351136
body = bodyStream.stream
11361137
contentLength = bodyStream.length
1137-
} else if (util.isBlobLike(body) && request.contentType == null && body.type) {
1138-
headers.push('content-type', body.type)
1138+
} else if (util.isBlobLike(body) && request.contentType == null) {
1139+
const contentType = body.type
1140+
if (contentType) {
1141+
const contentTypeValue = `${contentType}`
1142+
if (!util.isValidHeaderValue(contentTypeValue)) {
1143+
util.errorRequest(client, request, new InvalidArgumentError('invalid content-type header'))
1144+
return false
1145+
}
1146+
headers.push('content-type', contentTypeValue)
1147+
}
11391148
}
11401149

11411150
if (body && typeof body.read === 'function') {

test/request-crlf.js

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use strict'
22

3+
const assert = require('node:assert')
34
const { tspl } = require('@matteo.collina/tspl')
45
const { createServer } = require('node:http')
56
const { test, after } = require('node:test')
@@ -34,3 +35,33 @@ test('should validate content-type CRLF Injection', async (t) => {
3435
}
3536
await t.completed
3637
})
38+
39+
test('should validate blob body content-type CRLF Injection', async () => {
40+
let receivedRequest = false
41+
const server = createServer({ joinDuplicateHeaders: true }, (req, res) => {
42+
receivedRequest = true
43+
res.statusCode = 200
44+
res.end('hello')
45+
})
46+
47+
after(() => server.close())
48+
49+
server.listen(0)
50+
51+
await once(server, 'listening')
52+
53+
class MaliciousBlob extends Blob {
54+
get type () {
55+
return 'text/html\r\nX-Injected: true'
56+
}
57+
}
58+
59+
await assert.rejects(
60+
request(`http://localhost:${server.address().port}/endpoint`, {
61+
method: 'POST',
62+
body: new MaliciousBlob(['hello'])
63+
}),
64+
(e) => e instanceof errors.InvalidArgumentError && e.message === 'invalid content-type header'
65+
)
66+
assert.strictEqual(receivedRequest, false)
67+
})

0 commit comments

Comments
 (0)