fastify 3.29.1 → 3.29.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fastify.js +2 -2
- package/lib/contentTypeParser.js +23 -18
- package/package.json +1 -1
- package/test/content-parser.test.js +68 -2
- package/test/request-error.test.js +47 -1
package/fastify.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict'
|
|
2
2
|
|
|
3
|
-
const VERSION = '3.29.
|
|
3
|
+
const VERSION = '3.29.3'
|
|
4
4
|
|
|
5
5
|
const Avvio = require('avvio')
|
|
6
6
|
const http = require('http')
|
|
@@ -584,7 +584,7 @@ function fastify (options) {
|
|
|
584
584
|
// https://github.com/nodejs/node/blob/6ca23d7846cb47e84fd344543e394e50938540be/lib/_http_server.js#L666
|
|
585
585
|
|
|
586
586
|
// If the socket is not writable, there is no reason to try to send data.
|
|
587
|
-
if (socket.writable
|
|
587
|
+
if (socket.writable) {
|
|
588
588
|
socket.write(`HTTP/1.1 400 Bad Request\r\nContent-Length: ${body.length}\r\nContent-Type: application/json\r\n\r\n${body}`)
|
|
589
589
|
}
|
|
590
590
|
socket.destroy(err)
|
package/lib/contentTypeParser.js
CHANGED
|
@@ -32,9 +32,10 @@ const warning = require('./warnings')
|
|
|
32
32
|
|
|
33
33
|
function ContentTypeParser (bodyLimit, onProtoPoisoning, onConstructorPoisoning) {
|
|
34
34
|
this[kDefaultJsonParse] = getDefaultJsonParser(onProtoPoisoning, onConstructorPoisoning)
|
|
35
|
-
|
|
36
|
-
this.customParsers
|
|
37
|
-
this.customParsers
|
|
35
|
+
// using a map instead of a plain object to avoid prototype hijack attacks
|
|
36
|
+
this.customParsers = new Map()
|
|
37
|
+
this.customParsers.set('application/json', new Parser(true, false, bodyLimit, this[kDefaultJsonParse]))
|
|
38
|
+
this.customParsers.set('text/plain', new Parser(true, false, bodyLimit, defaultPlainTextParser))
|
|
38
39
|
this.parserList = ['application/json', 'text/plain']
|
|
39
40
|
this.parserRegExpList = []
|
|
40
41
|
this.cache = lru(100)
|
|
@@ -65,38 +66,42 @@ ContentTypeParser.prototype.add = function (contentType, opts, parserFn) {
|
|
|
65
66
|
)
|
|
66
67
|
|
|
67
68
|
if (contentTypeIsString && contentType === '*') {
|
|
68
|
-
this.customParsers
|
|
69
|
+
this.customParsers.set('', parser)
|
|
69
70
|
} else {
|
|
70
71
|
if (contentTypeIsString) {
|
|
71
72
|
this.parserList.unshift(contentType)
|
|
72
73
|
} else {
|
|
73
74
|
this.parserRegExpList.unshift(contentType)
|
|
74
75
|
}
|
|
75
|
-
this.customParsers
|
|
76
|
+
this.customParsers.set(contentType.toString(), parser)
|
|
76
77
|
}
|
|
77
78
|
}
|
|
78
79
|
|
|
79
80
|
ContentTypeParser.prototype.hasParser = function (contentType) {
|
|
80
|
-
return contentType
|
|
81
|
+
return this.customParsers.has(typeof contentType === 'string' ? contentType : contentType.toString())
|
|
81
82
|
}
|
|
82
83
|
|
|
83
84
|
ContentTypeParser.prototype.existingParser = function (contentType) {
|
|
84
|
-
if (contentType === 'application/json') {
|
|
85
|
-
return this.customParsers
|
|
85
|
+
if (contentType === 'application/json' && this.customParsers.has(contentType)) {
|
|
86
|
+
return this.customParsers.get(contentType).fn !== this[kDefaultJsonParse]
|
|
86
87
|
}
|
|
87
|
-
if (contentType === 'text/plain') {
|
|
88
|
-
return this.customParsers
|
|
88
|
+
if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
|
|
89
|
+
return this.customParsers.get(contentType).fn !== defaultPlainTextParser
|
|
89
90
|
}
|
|
90
91
|
|
|
91
|
-
return
|
|
92
|
+
return this.hasParser(contentType)
|
|
92
93
|
}
|
|
93
94
|
|
|
94
95
|
ContentTypeParser.prototype.getParser = function (contentType) {
|
|
96
|
+
if (this.hasParser(contentType)) {
|
|
97
|
+
return this.customParsers.get(contentType)
|
|
98
|
+
}
|
|
99
|
+
|
|
95
100
|
// eslint-disable-next-line no-var
|
|
96
101
|
for (var i = 0; i !== this.parserList.length; ++i) {
|
|
97
102
|
const parserName = this.parserList[i]
|
|
98
|
-
if (contentType.indexOf(parserName)
|
|
99
|
-
const parser = this.customParsers
|
|
103
|
+
if (contentType.indexOf(parserName) !== -1) {
|
|
104
|
+
const parser = this.customParsers.get(parserName)
|
|
100
105
|
this.cache.set(contentType, parser)
|
|
101
106
|
return parser
|
|
102
107
|
}
|
|
@@ -106,17 +111,17 @@ ContentTypeParser.prototype.getParser = function (contentType) {
|
|
|
106
111
|
for (var j = 0; j !== this.parserRegExpList.length; ++j) {
|
|
107
112
|
const parserRegExp = this.parserRegExpList[j]
|
|
108
113
|
if (parserRegExp.test(contentType)) {
|
|
109
|
-
const parser = this.customParsers
|
|
114
|
+
const parser = this.customParsers.get(parserRegExp.toString())
|
|
110
115
|
this.cache.set(contentType, parser)
|
|
111
116
|
return parser
|
|
112
117
|
}
|
|
113
118
|
}
|
|
114
119
|
|
|
115
|
-
return this.customParsers
|
|
120
|
+
return this.customParsers.get('')
|
|
116
121
|
}
|
|
117
122
|
|
|
118
123
|
ContentTypeParser.prototype.removeAll = function () {
|
|
119
|
-
this.customParsers =
|
|
124
|
+
this.customParsers = new Map()
|
|
120
125
|
this.parserRegExpList = []
|
|
121
126
|
this.parserList = []
|
|
122
127
|
this.cache = lru(100)
|
|
@@ -125,7 +130,7 @@ ContentTypeParser.prototype.removeAll = function () {
|
|
|
125
130
|
ContentTypeParser.prototype.remove = function (contentType) {
|
|
126
131
|
if (!(typeof contentType === 'string' || contentType instanceof RegExp)) throw new FST_ERR_CTP_INVALID_TYPE()
|
|
127
132
|
|
|
128
|
-
|
|
133
|
+
this.customParsers.delete(contentType.toString())
|
|
129
134
|
|
|
130
135
|
const parsers = typeof contentType === 'string' ? this.parserList : this.parserRegExpList
|
|
131
136
|
|
|
@@ -290,7 +295,7 @@ function Parser (asString, asBuffer, bodyLimit, fn) {
|
|
|
290
295
|
function buildContentTypeParser (c) {
|
|
291
296
|
const contentTypeParser = new ContentTypeParser()
|
|
292
297
|
contentTypeParser[kDefaultJsonParse] = c[kDefaultJsonParse]
|
|
293
|
-
|
|
298
|
+
contentTypeParser.customParsers = new Map(c.customParsers.entries())
|
|
294
299
|
contentTypeParser.parserList = c.parserList.slice()
|
|
295
300
|
return contentTypeParser
|
|
296
301
|
}
|
package/package.json
CHANGED
|
@@ -181,7 +181,7 @@ test('add', t => {
|
|
|
181
181
|
const contentTypeParser = fastify[keys.kContentTypeParser]
|
|
182
182
|
|
|
183
183
|
contentTypeParser.add('*', {}, first)
|
|
184
|
-
t.equal(contentTypeParser.customParsers
|
|
184
|
+
t.equal(contentTypeParser.customParsers.get('').fn, first)
|
|
185
185
|
})
|
|
186
186
|
|
|
187
187
|
t.end()
|
|
@@ -239,7 +239,7 @@ test('remove', t => {
|
|
|
239
239
|
|
|
240
240
|
contentTypeParser.remove('image/png')
|
|
241
241
|
|
|
242
|
-
t.same(
|
|
242
|
+
t.same(contentTypeParser.customParsers.size, 2)
|
|
243
243
|
})
|
|
244
244
|
|
|
245
245
|
t.end()
|
|
@@ -262,3 +262,69 @@ test('remove all should remove all existing parsers and reset cache', t => {
|
|
|
262
262
|
t.same(contentTypeParser.parserRegExpList.length, 0)
|
|
263
263
|
t.same(Object.keys(contentTypeParser.customParsers).length, 0)
|
|
264
264
|
})
|
|
265
|
+
|
|
266
|
+
test('Safeguard against malicious content-type / 1', async t => {
|
|
267
|
+
const badNames = Object.getOwnPropertyNames({}.__proto__) // eslint-disable-line
|
|
268
|
+
t.plan(badNames.length)
|
|
269
|
+
|
|
270
|
+
const fastify = Fastify()
|
|
271
|
+
|
|
272
|
+
fastify.post('/', async () => {
|
|
273
|
+
return 'ok'
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
for (const prop of badNames) {
|
|
277
|
+
const response = await fastify.inject({
|
|
278
|
+
method: 'POST',
|
|
279
|
+
path: '/',
|
|
280
|
+
headers: {
|
|
281
|
+
'content-type': prop
|
|
282
|
+
},
|
|
283
|
+
body: ''
|
|
284
|
+
})
|
|
285
|
+
|
|
286
|
+
t.same(response.statusCode, 415)
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
test('Safeguard against malicious content-type / 2', async t => {
|
|
291
|
+
t.plan(1)
|
|
292
|
+
|
|
293
|
+
const fastify = Fastify()
|
|
294
|
+
|
|
295
|
+
fastify.post('/', async () => {
|
|
296
|
+
return 'ok'
|
|
297
|
+
})
|
|
298
|
+
|
|
299
|
+
const response = await fastify.inject({
|
|
300
|
+
method: 'POST',
|
|
301
|
+
path: '/',
|
|
302
|
+
headers: {
|
|
303
|
+
'content-type': '\\u0063\\u006fnstructor'
|
|
304
|
+
},
|
|
305
|
+
body: ''
|
|
306
|
+
})
|
|
307
|
+
|
|
308
|
+
t.same(response.statusCode, 415)
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
test('Safeguard against malicious content-type / 3', async t => {
|
|
312
|
+
t.plan(1)
|
|
313
|
+
|
|
314
|
+
const fastify = Fastify()
|
|
315
|
+
|
|
316
|
+
fastify.post('/', async () => {
|
|
317
|
+
return 'ok'
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
const response = await fastify.inject({
|
|
321
|
+
method: 'POST',
|
|
322
|
+
path: '/',
|
|
323
|
+
headers: {
|
|
324
|
+
'content-type': 'constructor; charset=utf-8'
|
|
325
|
+
},
|
|
326
|
+
body: ''
|
|
327
|
+
})
|
|
328
|
+
|
|
329
|
+
t.same(response.statusCode, 415)
|
|
330
|
+
})
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { connect } = require('net')
|
|
4
4
|
const t = require('tap')
|
|
5
|
+
const semver = require('semver')
|
|
5
6
|
const test = t.test
|
|
6
7
|
const Fastify = require('..')
|
|
7
8
|
const { kRequest } = require('../lib/symbols.js')
|
|
@@ -153,7 +154,7 @@ test('default clientError handler ignores sockets in destroyed state', t => {
|
|
|
153
154
|
})
|
|
154
155
|
|
|
155
156
|
test('default clientError handler destroys sockets in writable state', t => {
|
|
156
|
-
t.plan(
|
|
157
|
+
t.plan(2)
|
|
157
158
|
|
|
158
159
|
const fastify = Fastify({
|
|
159
160
|
bodyLimit: 1,
|
|
@@ -169,6 +170,9 @@ test('default clientError handler destroys sockets in writable state', t => {
|
|
|
169
170
|
},
|
|
170
171
|
destroy () {
|
|
171
172
|
t.pass('destroy should be called')
|
|
173
|
+
},
|
|
174
|
+
write (response) {
|
|
175
|
+
t.match(response, /^HTTP\/1.1 400 Bad Request/)
|
|
172
176
|
}
|
|
173
177
|
})
|
|
174
178
|
})
|
|
@@ -189,6 +193,9 @@ test('default clientError handler destroys http sockets in non-writable state',
|
|
|
189
193
|
},
|
|
190
194
|
destroy () {
|
|
191
195
|
t.pass('destroy should be called')
|
|
196
|
+
},
|
|
197
|
+
write (response) {
|
|
198
|
+
t.fail('write should not be called')
|
|
192
199
|
}
|
|
193
200
|
})
|
|
194
201
|
})
|
|
@@ -273,3 +280,42 @@ test('encapsulated error handler binding', t => {
|
|
|
273
280
|
t.equal(fastify.hello, undefined)
|
|
274
281
|
})
|
|
275
282
|
})
|
|
283
|
+
|
|
284
|
+
const skip = semver.lt(process.versions.node, '11.0.0')
|
|
285
|
+
|
|
286
|
+
test('default clientError replies with bad request on reused keep-alive connection', { skip }, t => {
|
|
287
|
+
t.plan(2)
|
|
288
|
+
|
|
289
|
+
let response = ''
|
|
290
|
+
|
|
291
|
+
const fastify = Fastify({
|
|
292
|
+
bodyLimit: 1,
|
|
293
|
+
keepAliveTimeout: 100
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
fastify.get('/', (request, reply) => {
|
|
297
|
+
reply.send('OK\n')
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
fastify.listen({ port: 0 }, function (err) {
|
|
301
|
+
t.error(err)
|
|
302
|
+
fastify.server.unref()
|
|
303
|
+
|
|
304
|
+
const client = connect(fastify.server.address().port)
|
|
305
|
+
|
|
306
|
+
client.on('data', chunk => {
|
|
307
|
+
response += chunk.toString('utf-8')
|
|
308
|
+
})
|
|
309
|
+
|
|
310
|
+
client.on('end', () => {
|
|
311
|
+
t.match(response, /^HTTP\/1.1 200 OK.*HTTP\/1.1 400 Bad Request/s)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
client.resume()
|
|
315
|
+
client.write('GET / HTTP/1.1\r\n')
|
|
316
|
+
client.write('\r\n\r\n')
|
|
317
|
+
client.write('GET /?a b HTTP/1.1\r\n')
|
|
318
|
+
client.write('Connection: close\r\n')
|
|
319
|
+
client.write('\r\n\r\n')
|
|
320
|
+
})
|
|
321
|
+
})
|