Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

introduce headerSize limit #64

Merged
merged 7 commits into from
Dec 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

Major changes since the last busboy release (0.31):
Major changes since the last busboy release (0.3.1):

# 1.0.0 - TBD, 2021

Expand All @@ -11,4 +11,5 @@ Major changes since the last busboy release (0.31):
* Tests were converted to Mocha (#11, #12, #22, #23)
* Add isPartAFile-option, to make the file-detection configurable (#53)
* Empty Parts will not hang the process (#55)
* FileStreams also provide the property `bytesRead`
* FileStreams also provide the property `bytesRead` (#51)
* add and expose headerSize limit (#64)
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ Busboy methods

* **parts** - _integer_ - For multipart forms, the max number of parts (fields + files) (Default: Infinity).

* **headerPairs** - _integer_ - For multipart forms, the max number of header key=>value pairs to parse **Default:** 2000 (same as node's http).
* **headerPairs** - _integer_ - For multipart forms, the max number of header key=>value pairs to parse **Default:** 2000

* **headerSize** - _integer_ - For multipart forms, the max size of a multipart header **Default:** 81920.

* The constructor can throw errors:

Expand Down
18 changes: 8 additions & 10 deletions deps/dicer/lib/HeaderParser.js
Original file line number Diff line number Diff line change
@@ -1,37 +1,35 @@
const EventEmitter = require('events').EventEmitter
const inherits = require('util').inherits
const getLimit = require('../../../lib/utils').getLimit

const StreamSearch = require('../../streamsearch/sbmh')

const B_DCRLF = Buffer.from('\r\n\r\n')
const RE_CRLF = /\r\n/g
const RE_HDR = /^([^:]+):[ \t]?([\x00-\xFF]+)?$/ // eslint-disable-line no-control-regex
const MAX_HEADER_PAIRS = 2000 // from node's http.js
const MAX_HEADER_SIZE = 80 * 1024 // from node's http_parser

function HeaderParser (cfg) {
EventEmitter.call(this)

cfg = cfg || {}
const self = this
this.nread = 0
this.maxed = false
this.npairs = 0
this.maxHeaderPairs = (cfg && typeof cfg.maxHeaderPairs === 'number'
? cfg.maxHeaderPairs
: MAX_HEADER_PAIRS)
this.maxHeaderPairs = getLimit(cfg, 'maxHeaderPairs', 2000)
this.maxHeaderSize = getLimit(cfg, 'maxHeaderSize', 80 * 1024)
this.buffer = ''
this.header = {}
this.finished = false
this.ss = new StreamSearch(B_DCRLF)
this.ss.on('info', function (isMatch, data, start, end) {
if (data && !self.maxed) {
if (self.nread + (end - start) > MAX_HEADER_SIZE) {
end = MAX_HEADER_SIZE - self.nread + start
self.nread = MAX_HEADER_SIZE
if (self.nread + end - start >= self.maxHeaderSize) {
end = self.maxHeaderSize - self.nread + start
self.nread = self.maxHeaderSize
self.maxed = true
} else { self.nread += (end - start) }

if (self.nread === MAX_HEADER_SIZE) { self.maxed = true }

self.buffer += data.toString('binary', start, end)
}
if (isMatch) { self._finish() }
Expand Down
8 changes: 7 additions & 1 deletion lib/main.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,15 @@ export interface BusboyConfig {
parts?: number | undefined;
/**
* For multipart forms, the max number of header key=>value pairs to parse
* @default 2000 (same as node's http)
* @default 2000
*/
headerPairs?: number | undefined;

/**
* For multipart forms, the max size of a header part
* @default 81920
*/
headerSize?: number | undefined;
}
| undefined;
}
Expand Down
2 changes: 2 additions & 0 deletions lib/types/multipart.js
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function Multipart (boy, cfg) {
const fieldsLimit = getLimit(limits, 'fields', Infinity)
const partsLimit = getLimit(limits, 'parts', Infinity)
const headerPairsLimit = getLimit(limits, 'headerPairs', 2000)
const headerSizeLimit = getLimit(limits, 'headerSize', 80 * 1024)

let nfiles = 0
let nfields = 0
Expand All @@ -78,6 +79,7 @@ function Multipart (boy, cfg) {
const parserCfg = {
boundary: boundary,
maxHeaderPairs: headerPairsLimit,
maxHeaderSize: headerSizeLimit,
partHwm: fileOpts.highWaterMark,
highWaterMark: cfg.highWaterMark
}
Expand Down
94 changes: 93 additions & 1 deletion test/dicer-headerparser.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,13 +19,101 @@ describe('dicer-headerparser', () => {
expected: { 'content-type': [' text/plain'], 'content-length': ['0'] },
what: 'Value spacing'
},
{
source: ['Content-Type:\t text/plain',
'Content-Length:0'
].join('\r\n') + DCRLF,
cfg: {
maxHeaderPairs: 0
},
expected: { },
what: 'should enforce maxHeaderPairs of 0'
},
{
source: ['Content-Type:\t text/plain',
'Content-Length:0'
].join('\r\n') + DCRLF,
cfg: {
maxHeaderPairs: 1
},
expected: { 'content-type': [' text/plain'] },
what: 'should enforce maxHeaderPairs of 1'
},
{
source: ['Content-Type:\r\n text/plain',
'Foo:\r\n bar\r\n baz'
].join('\r\n') + DCRLF,
expected: {},
cfg: {
maxHeaderSize: 0
},
what: 'should enforce maxHeaderSize of 0'
},
{
source: ['Content-Type:\r\n text/plain',
'Foo:\r\n bar\r\n baz'
].join('\r\n') + DCRLF,
expected: { 'content-type': [' text/plai'] },
cfg: {
maxHeaderSize: 25
},
what: 'should enforce maxHeaderSize of 25'
},
{
source: ['Content-Type:\r\n text/plain',
'Foo:\r\n bar\r\n baz'
].join('\r\n') + DCRLF,
expected: { 'content-type': [' text/plain'] },
cfg: {
maxHeaderSize: 31
},
what: 'should enforce maxHeaderSize of 31 and ignore the second header'
},
{
source: ['Content-Type:\r\n text/plain',
'Foo:\r\n bar\r\n baz'
].join('\r\n') + DCRLF,
expected: { 'content-type': [' text/plain'], foo: [''] },
cfg: {
maxHeaderSize: 32
},
what: 'should enforce maxHeaderSize of 32 and only add key of second header'
},
{
source: ['Content-Type:\r\n text/plain',
'Foo:\r\n bar\r\n baz'
].join('\r\n') + DCRLF,
expected: { 'content-type': [' text/plain'], foo: ['\r'] },
cfg: {
maxHeaderSize: 33
},
what: 'should enforce maxHeaderSize of 32 and get only first character of second pair'
},
{
source: ['Content-Type:\t text/plain',
'Content-Length:0'
].join('\r\n') + DCRLF,
cfg: {
maxHeaderPairs: 2
},
expected: { 'content-type': [' text/plain'], 'content-length': ['0'] },
what: 'should enforce maxHeaderPairs of 2'
},
{
source: ['Content-Type:\r\n text/plain',
'Foo:\r\n bar\r\n baz'
].join('\r\n') + DCRLF,
expected: { 'content-type': [' text/plain'], foo: [' bar baz'] },
what: 'Folded values'
},
{
source: [
'Foo: bar',
'Foo: baz'
].join('\r\n') + DCRLF,
expected: { foo: ['bar', 'baz'] },
what: 'Folded values'
},
{
source: ['Content-Type:',
'Foo: '
Expand All @@ -50,7 +138,11 @@ describe('dicer-headerparser', () => {
}
].forEach(function (v) {
it(v.what, (done) => {
const parser = new HeaderParser()
const cfg = {
...v.cfg
}

const parser = Object.keys(cfg).length ? new HeaderParser(cfg) : new HeaderParser()
let fired = false

parser.on('header', function (header) {
Expand Down
1 change: 1 addition & 0 deletions test/types/main.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { fileSize: 200
new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { files: 200 } }); // $ExpectType Busboy
new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { parts: 200 } }); // $ExpectType Busboy
new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerPairs: 200 } }); // $ExpectType Busboy
new BusboyDefault({ headers: { 'content-type': 'foo' }, limits: { headerSize: 200 } }); // $ExpectType Busboy
new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fieldName === 'my-special-field' || fileName !== 'not-so-special.txt' }); // $ExpectType Busboy
new BusboyDefault({ headers: { 'content-type': 'foo' }, isPartAFile: (fieldName, contentType, fileName) => fileName !== undefined }); // $ExpectType Busboy

Expand Down