180 lines
7.3 KiB
Diff
180 lines
7.3 KiB
Diff
--- 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');
|
|
|
|
--- test/index.js
|
|
@@ -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);
|
|
});
|
|
|
|
@@ -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()', () => {
|
|
@@ -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');
|
|
+ });
|
|
});
|