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 CHANGED
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const VERSION = '3.29.1'
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 && socket.bytesWritten === 0) {
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)
@@ -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
- this.customParsers = {}
36
- this.customParsers['application/json'] = new Parser(true, false, bodyLimit, this[kDefaultJsonParse])
37
- this.customParsers['text/plain'] = new Parser(true, false, bodyLimit, defaultPlainTextParser)
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[''] = parser
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[contentType] = parser
76
+ this.customParsers.set(contentType.toString(), parser)
76
77
  }
77
78
  }
78
79
 
79
80
  ContentTypeParser.prototype.hasParser = function (contentType) {
80
- return contentType in this.customParsers
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['application/json'] && this.customParsers['application/json'].fn !== this[kDefaultJsonParse]
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['text/plain'] && this.customParsers['text/plain'].fn !== defaultPlainTextParser
88
+ if (contentType === 'text/plain' && this.customParsers.has(contentType)) {
89
+ return this.customParsers.get(contentType).fn !== defaultPlainTextParser
89
90
  }
90
91
 
91
- return contentType in this.customParsers
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) > -1) {
99
- const parser = this.customParsers[parserName]
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[parserRegExp]
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
- delete this.customParsers[contentType]
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
- Object.assign(contentTypeParser.customParsers, c.customParsers)
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fastify",
3
- "version": "3.29.1",
3
+ "version": "3.29.3",
4
4
  "description": "Fast and low overhead web framework, for Node.js",
5
5
  "main": "fastify.js",
6
6
  "type": "commonjs",
@@ -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[''].fn, first)
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(Object.keys(contentTypeParser.customParsers).length, 2)
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(1)
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
+ })