Skip to content

Commit

Permalink
add parameterLimit option to urlencoded parser
Browse files Browse the repository at this point in the history
closes #41
  • Loading branch information
dougwilson committed Sep 2, 2014
1 parent 30a08ab commit 851cbfd
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 5 deletions.
6 changes: 6 additions & 0 deletions HISTORY.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
unreleased
==========

* add `parameterLimit` option to `urlencoded` parser
* respond with 415 when over `parameterLimit` in `urlencoded`

1.6.7 / 2014-08-29
==================

Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,11 +108,14 @@ The options are:
- `extended` - parse extended syntax with the [qs](https://www.npmjs.org/package/qs#readme) module. (default: `true`)
- `inflate` - if deflated bodies will be inflated. (default: `true`)
- `limit` - maximum request body size. (default: `<100kb>`)
- `parameterLimit` - maximum number of parameters. (default: `1000`)
- `type` - request content-type to parse (default: `urlencoded`)
- `verify` - function to verify body content

The `extended` argument allows to choose between parsing the urlencoded data with the `querystring` library (when `false`) or the `qs` library (when `true`). The "extended" syntax allows for rich objects and arrays to be encoded into the urlencoded format, allowing for a JSON-like experience with urlencoded. For more information, please [see the qs library](https://www.npmjs.org/package/qs#readme).

The `parameterLimit` argument controls the maximum number of parameters that are allowed in the urlencoded data. If a request contains more parameters than this value, a 415 will be returned to the client.

The `type` argument is passed directly to the [type-is](https://www.npmjs.org/package/type-is#readme) library. This can be an extension name (like `urlencoded`), a mime type (like `application/x-www-form-urlencoded`), or a mime time with a wildcard (like `*/x-www-form-urlencoded`).

The `verify` argument, if supplied, is called as `verify(req, res, buf, encoding)`, where `buf` is a `Buffer` of the raw request body and `encoding` is the encoding of the request. The parsing can be aborted by throwing an error.
Expand Down
8 changes: 5 additions & 3 deletions lib/read.js
Original file line number Diff line number Diff line change
Expand Up @@ -89,9 +89,11 @@ function read(req, res, next, parse, options) {
? iconv.decode(body, encoding)
: body
req.body = parse(body)
} catch (err){
err.body = body
err.status = 400
} catch (err) {
if (!err.status) {
err.body = body
err.status = 400
}
return next(err)
}

Expand Down
94 changes: 92 additions & 2 deletions lib/types/urlencoded.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,8 @@ function urlencoded(options){
}

var queryparse = extended
? parser('qs')
: parser('querystring')
? extendedparser(options)
: simpleparser(options)

function parse(body) {
return body.length
Expand Down Expand Up @@ -89,6 +89,65 @@ function urlencoded(options){
}
}

/**
* Get the extended query parser.
*
* @param {object} options
*/

function extendedparser(options) {
var parameterLimit = options.parameterLimit !== undefined
? options.parameterLimit
: 1000
var parse = parser('qs')

if (isNaN(parameterLimit) || parameterLimit < 1) {
throw new TypeError('option parameterLimit must be a positive number')
}

if (isFinite(parameterLimit)) {
parameterLimit = parameterLimit | 0
}

return function queryparse(body) {
if (overlimit(body, parameterLimit)) {
var err = new Error('too many parameters')
err.status = 413
throw err
}

return parse(body, {parameterLimit: parameterLimit})
}
}

/**
* Determine if the parameter count is over the limit.
*
* @param {string} body
* @param {number} limit
* @api private
*/

function overlimit(body, limit) {
if (limit === Infinity) {
return false
}

var count = 0
var index = 0

while ((index = body.indexOf('&', index)) !== -1) {
count++
index++

if (count === limit) {
return true
}
}

return false
}

/**
* Get parser for module name dynamically.
*
Expand All @@ -109,3 +168,34 @@ function parser(name) {

return mod.parse
}

/**
* Get the simple query parser.
*
* @param {object} options
*/

function simpleparser(options) {
var parameterLimit = options.parameterLimit !== undefined
? options.parameterLimit
: 1000
var parse = parser('querystring')

if (isNaN(parameterLimit) || parameterLimit < 1) {
throw new TypeError('option parameterLimit must be a positive number')
}

if (isFinite(parameterLimit)) {
parameterLimit = parameterLimit | 0
}

return function queryparse(body) {
if (overlimit(body, parameterLimit)) {
var err = new Error('too many parameters')
err.status = 413
throw err
}

return parse(body, undefined, undefined, {maxKeys: parameterLimit})
}
}
131 changes: 131 additions & 0 deletions test/urlencoded.js
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,114 @@ describe('bodyParser.urlencoded()', function(){
})
})

describe('with parameterLimit option', function () {
describe('with extended: false', function () {
it('should reject 0', function () {
assert.throws(createServer.bind(null, { extended: false, parameterLimit: 0 }), /option parameterLimit/)
})

it('should reject string', function () {
assert.throws(createServer.bind(null, { extended: false, parameterLimit: 'beep' }), /option parameterLimit/)
})

it('should 415 if over limit', function (done) {
request(createServer({ extended: false, parameterLimit: 10 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11))
.expect(413, /too many parameters/, done)
})

it('should work when at the limit', function (done) {
request(createServer({ extended: false, parameterLimit: 10 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(10))
.expect(expectKeyCount(10))
.expect(200, done)
})

it('should work if number is floating point', function (done) {
request(createServer({ extended: false, parameterLimit: 10.1 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11))
.expect(413, /too many parameters/, done)
})

it('should work with large limit', function (done) {
request(createServer({ extended: false, parameterLimit: 5000 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(5000))
.expect(expectKeyCount(5000))
.expect(200, done)
})

it('should work with Infinity limit', function (done) {
request(createServer({ extended: false, parameterLimit: Infinity }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(10000))
.expect(expectKeyCount(10000))
.expect(200, done)
})
})

describe('with extended: true', function () {
it('should reject 0', function () {
assert.throws(createServer.bind(null, { extended: true, parameterLimit: 0 }), /option parameterLimit/)
})

it('should reject string', function () {
assert.throws(createServer.bind(null, { extended: true, parameterLimit: 'beep' }), /option parameterLimit/)
})

it('should 415 if over limit', function (done) {
request(createServer({ extended: true, parameterLimit: 10 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11))
.expect(413, /too many parameters/, done)
})

it('should work when at the limit', function (done) {
request(createServer({ extended: true, parameterLimit: 10 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(10))
.expect(expectKeyCount(10))
.expect(200, done)
})

it('should work if number is floating point', function (done) {
request(createServer({ extended: true, parameterLimit: 10.1 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(11))
.expect(413, /too many parameters/, done)
})

it('should work with large limit', function (done) {
request(createServer({ extended: true, parameterLimit: 5000 }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(5000))
.expect(expectKeyCount(5000))
.expect(200, done)
})

it('should work with Infinity limit', function (done) {
request(createServer({ extended: true, parameterLimit: Infinity }))
.post('/')
.set('Content-Type', 'application/x-www-form-urlencoded')
.send(createManyParams(10000))
.expect(expectKeyCount(10000))
.expect(200, done)
})
})
})

describe('with type option', function(){
var server;
before(function(){
Expand Down Expand Up @@ -371,6 +479,23 @@ describe('bodyParser.urlencoded()', function(){
})
})

function createManyParams(count) {
var str = ''

if (count === 0) {
return str
}

str += '0=0'

for (var i = 1; i < count; i++) {
var n = i.toString(36)
str += '&' + n + '=' + n
}

return str
}

function createServer(opts){
var _bodyParser = bodyParser.urlencoded(opts)

Expand All @@ -381,3 +506,9 @@ function createServer(opts){
})
})
}

function expectKeyCount(count) {
return function (res) {
assert.equal(Object.keys(JSON.parse(res.text)).length, count)
}
}

0 comments on commit 851cbfd

Please sign in to comment.