Skip to content
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
32 changes: 29 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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');
}
}
}

Expand All @@ -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');
}
}
}

Expand Down Expand Up @@ -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) {
Expand All @@ -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');
Expand Down
78 changes: 76 additions & 2 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -117,15 +117,15 @@ 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);
});

it('handles multiple charset params', () => {

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);
});

Expand All @@ -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/<html>')).to.throw('Invalid content-type header');
});
});

describe('disposition()', () => {
Expand Down Expand Up @@ -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');
});
});
Loading