Skip to content

Commit a0ed00b

Browse files
authored
Merge pull request #122 from BackendStack21/fix/error-status-code-validation
fix: validate error-derived status codes to prevent process crash (DoS)
2 parents 18adfab + 345141c commit a0ed00b

4 files changed

Lines changed: 122 additions & 8 deletions

File tree

index.js

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
const requestRouter = require('./libs/request-router')
1010
const applySecurityHeaders = require('./libs/security-headers')
11-
const { deepFreezeObject } = require('./libs/utils')
11+
const { deepFreezeObject, toHttpStatusCode } = require('./libs/utils')
1212
const exts = {
1313
request: {},
1414
response: require('./libs/response-extensions')
@@ -18,9 +18,7 @@ module.exports = (options = {}) => {
1818
options.errorHandler =
1919
options.errorHandler ||
2020
((err, req, res) => {
21-
const statusCode = typeof (err.status || err.code || err.statusCode) === 'number'
22-
? (err.status || err.code || err.statusCode)
23-
: 500
21+
const statusCode = toHttpStatusCode(err.status || err.code || err.statusCode)
2422
res.send({ code: statusCode, message: 'Internal Server Error' }, statusCode)
2523
})
2624

libs/response-extensions.js

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

3-
const { forEachObject } = require('./utils')
3+
const { forEachObject, toHttpStatusCode } = require('./utils')
44

55
const CONTENT_TYPE_HEADER = 'content-type'
66
const TYPE_JSON = 'application/json; charset=utf-8'
@@ -32,14 +32,13 @@ const beforeEnd = (res, contentType, statusCode, data) => {
3232
if (contentType) {
3333
res.setHeader(CONTENT_TYPE_HEADER, contentType)
3434
}
35-
res.statusCode = statusCode
35+
res.statusCode = toHttpStatusCode(statusCode, res.statusCode)
3636
}
3737

3838
const isProduction = () => process.env.NODE_ENV === 'production'
3939

4040
const parseErr = error => {
41-
const errorCode = error.status || error.code || error.statusCode
42-
const statusCode = typeof errorCode === 'number' ? errorCode : 500
41+
const statusCode = toHttpStatusCode(error.status || error.code || error.statusCode)
4342

4443
if (isProduction()) {
4544
return {

libs/utils.js

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

3+
/**
4+
* Coerces a value into a valid HTTP status code.
5+
* Returns the fallback when the value is not an integer in the 100-999 range,
6+
* preventing `RangeError: Invalid status code` crashes on res.statusCode.
7+
*
8+
* @param {*} code
9+
* @param {number} [fallback=500]
10+
* @returns {number}
11+
*/
12+
module.exports.toHttpStatusCode = (code, fallback = 500) => {
13+
return Number.isInteger(code) && code >= 100 && code <= 999 ? code : fallback
14+
}
15+
316
module.exports.forEachObject = (obj, cb) => {
417
const keys = Object.keys(obj)
518
const length = keys.length

specs/security.test.js

Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -310,4 +310,108 @@ describe('Security Fixes', () => {
310310
await service.close()
311311
})
312312
})
313+
314+
describe('SEC-005: Out-of-range error status codes fall back to 500', () => {
315+
let server
316+
const service = require('../index')({
317+
errorHandler (err, req, res) {
318+
res.send(err)
319+
}
320+
})
321+
322+
service.get('/hello', (req, res) => {
323+
res.send('world')
324+
})
325+
326+
service.get('/error-code-99', (req, res) => {
327+
const err = new Error('Low code')
328+
err.code = 99
329+
throw err
330+
})
331+
332+
service.get('/error-status-huge', (req, res) => {
333+
const err = new Error('Huge status')
334+
err.status = 12345
335+
throw err
336+
})
337+
338+
service.get('/error-float-code', (req, res) => {
339+
const err = new Error('Float code')
340+
err.code = 200.5
341+
throw err
342+
})
343+
344+
it('should start service', async () => {
345+
server = await service.start(~~process.env.PORT)
346+
})
347+
348+
it('should return 500 instead of crashing when err.code is out of range', async () => {
349+
await request(server)
350+
.get('/error-code-99')
351+
.expect(500)
352+
.then((response) => {
353+
expect(response.body.code).to.equal(500)
354+
})
355+
})
356+
357+
it('should return 500 instead of crashing when err.status exceeds 999', async () => {
358+
await request(server)
359+
.get('/error-status-huge')
360+
.expect(500)
361+
.then((response) => {
362+
expect(response.body.code).to.equal(500)
363+
})
364+
})
365+
366+
it('should return 500 instead of crashing when err.code is not an integer', async () => {
367+
await request(server)
368+
.get('/error-float-code')
369+
.expect(500)
370+
.then((response) => {
371+
expect(response.body.code).to.equal(500)
372+
})
373+
})
374+
375+
it('server should still be alive and responsive after out-of-range errors', async () => {
376+
await request(server)
377+
.get('/hello')
378+
.expect(200)
379+
.then((response) => {
380+
expect(response.text).to.equal('world')
381+
})
382+
})
383+
384+
it('should successfully terminate the service', async () => {
385+
await service.close()
386+
})
387+
})
388+
389+
describe('SEC-005b: Default error handler also survives out-of-range codes', () => {
390+
let server
391+
const service = require('../index')()
392+
393+
service.get('/error-bad-code', (req, res) => {
394+
const err = new Error('boom')
395+
err.code = 99
396+
throw err
397+
})
398+
399+
it('should start service', async () => {
400+
server = await service.start(~~process.env.PORT)
401+
})
402+
403+
it('should return 500 with generic message and keep the process alive', async () => {
404+
await request(server)
405+
.get('/error-bad-code')
406+
.expect(500)
407+
.then((response) => {
408+
expect(response.body.code).to.equal(500)
409+
expect(response.body.message).to.equal('Internal Server Error')
410+
})
411+
})
412+
413+
it('should successfully terminate the service', async () => {
414+
await service.close()
415+
})
416+
})
313417
})

0 commit comments

Comments
 (0)