diff --git a/lib/index.js b/lib/index.js index fd0aa71..6ae94a4 100755 --- a/lib/index.js +++ b/lib/index.js @@ -15,8 +15,8 @@ const internals = {}; parameter = token "=" ( token / quoted-string ) */ -// 1: type/subtype 2: params -internals.contentTypeRegex = /^([^\/\s]+\/[^\s;]+)([ \t;][^\r\n]*)?$/; +// 1: type/subtype 2: params +internals.contentTypeRegex = /^([A-Za-z0-9!#$%&'*+.^_`|~-]+\/[A-Za-z0-9!#$%&'*+.^_`|~-]+)([ \t;][^\r\n]*)?$/; // 1: "b" 2: b internals.charsetParamRegex = /;\s*charset=(?:"([^"]+)"|([^;"\s]+))/i; @@ -45,6 +45,10 @@ exports.type = function (header) { const param = params.match(internals.charsetParamRegex); if (param) { result.charset = (param[1] || param[2]).toLowerCase(); + + if (internals.charsetParamRegex.test(params.slice(param.index + param[0].length))) { + throw Boom.badRequest('Invalid content-type header: duplicate parameter'); + } } } @@ -53,6 +57,10 @@ exports.type = function (header) { const param = params.match(internals.boundaryParamRegex); if (param) { result.boundary = param[1] || param[2]; + + if (internals.boundaryParamRegex.test(params.slice(param.index + param[0].length))) { + throw Boom.badRequest('Invalid content-type header: duplicate parameter'); + } } } @@ -104,12 +112,30 @@ exports.disposition = function (header) { } const result = {}; + const extKeys = new Set(); parameters.replace(internals.contentDispositionParamRegex, ($0, $1, $2, $3, $4, $5) => { if ($1 === '__proto__') { throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters'); } + if (result[$1] !== undefined) { + if ($2 && !extKeys.has($1)) { + // ext-value overriding regular value - allowed per RFC 6266 Section 4.3 + } + else if (!$2 && extKeys.has($1)) { + // Regular value after ext-value - ignore (keep ext-value) + return; + } + else { + throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters'); + } + } + + if ($2) { + extKeys.add($1); + } + let value; if ($2) { @@ -118,7 +144,7 @@ exports.disposition = function (header) { } try { - value = decodeURIComponent($3.split('\'')[2]); + value = decodeURIComponent($3.slice($3.indexOf('\'', $3.indexOf('\'') + 1) + 1)); } catch (err) { throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters'); diff --git a/test/index.js b/test/index.js index c386c26..679b6eb 100755 --- a/test/index.js +++ b/test/index.js @@ -117,7 +117,7 @@ describe('type()', () => { const header = `multipart/form-data ${new Array(80000).join(';boundary=#')}`; const now = Date.now(); - Content.type(header); + expect(() => Content.type(header)).to.throw('Invalid content-type header: duplicate parameter'); expect(Date.now() - now).to.be.below(100); }); @@ -125,7 +125,7 @@ describe('type()', () => { const header = `text/plain ${new Array(80000).join(';charset=utf-8')}`; const now = Date.now(); - Content.type(header); + expect(() => Content.type(header)).to.throw('Invalid content-type header: duplicate parameter'); expect(Date.now() - now).to.be.below(100); }); @@ -141,6 +141,26 @@ describe('type()', () => { expect(() => Content.type('application/json\n; charset=utf-8')).to.throw('Invalid content-type header'); }); + + it('errors on duplicate charset parameter', () => { + + expect(() => Content.type('text/plain; charset=utf-8; charset=ascii')).to.throw('Invalid content-type header: duplicate parameter'); + }); + + it('errors on duplicate boundary parameter', () => { + + expect(() => Content.type('multipart/form-data; boundary=abc; boundary=def')).to.throw('Invalid content-type header: duplicate parameter'); + }); + + it('errors on invalid token characters in type', () => { + + expect(() => Content.type('text/html\x00')).to.throw('Invalid content-type header'); + }); + + it('errors on invalid token characters in subtype', () => { + + expect(() => Content.type('text/')).to.throw('Invalid content-type header'); + }); }); describe('disposition()', () => { @@ -351,4 +371,58 @@ describe('disposition()', () => { const header = 'form-data; name="file"; filename="test.jpg" '; expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' }); }); + + it('errors on constructor param', () => { + + const header = 'form-data; name="file"; constructor=test'; + expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters'); + }); + + it('errors on toString param', () => { + + const header = 'form-data; name="file"; toString=test'; + expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters'); + }); + + it('handles single quote in ext-value', () => { + + const header = 'form-data; name="file"; filename*=utf-8\'en\'it\'s%20here.php'; + expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'it\'s here.php' }); + }); + + it('errors on duplicate parameter names', () => { + + const header = 'form-data; name="file"; filename="safe.txt"; filename="shell.php"'; + expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters'); + }); + + it('allows ext-value to override filename parameter', () => { + + const header = 'form-data; name="file"; filename="fallback.jpg"; filename*=utf-8\'\'extended.jpg'; + expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'extended.jpg' }); + }); + + it('keeps ext-value when filename parameter follows', () => { + + const header = 'form-data; name="file"; filename*=utf-8\'\'extended.jpg; filename="fallback.jpg"'; + expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'extended.jpg' }); + }); + + it('errors on duplicate ext-value parameters', () => { + + const header = 'form-data; name="file"; filename*=utf-8\'\'a.jpg; filename*=utf-8\'\'b.jpg'; + expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters'); + }); + + it('errors on duplicate even when ext-value override is present', () => { + + const header = 'form-data; name="avatar"; name*=utf-8\'\'admin; filename="safe.txt"; filename="shell.php"'; + expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters'); + }); + + it('errors on duplicate name parameters', () => { + + const header = 'form-data; name="avatar"; name="admin"'; + expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters'); + }); });