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
6 changes: 3 additions & 3 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ const internals = {};
*/

// 1: type/subtype 2: params
internals.contentTypeRegex = /^([^\/\s]+\/[^\s;]+)(.*)?$/;
internals.contentTypeRegex = /^([^\/\s]+\/[^\s;]+)([ \t;][^\r\n]*)?$/;

// 1: "b" 2: b
internals.charsetParamRegex = /;\s*charset=(?:"([^"]+)"|([^;"\s]+))/i;
Expand Down Expand Up @@ -82,10 +82,10 @@ exports.type = function (header) {
*/


internals.contentDispositionRegex = /^\s*form-data\s*(?:;\s*(.+))?$/i;
internals.contentDispositionRegex = /^\s*form-data\s*(?:;\s*(\S.*))?$/i;

// 1: name 2: * 3: ext-value 4: quoted 5: token
internals.contentDispositionParamRegex = /([^\=\*\s]+)(\*)?\s*\=\s*(?:([^;'"\s]+\'[\w-]*\'[^;\s]+)|(?:\"([^"]*)\")|([^;\s]*))(?:\s*(?:;\s*)|$)/g;
internals.contentDispositionParamRegex = /([^\=\*\s]+)(\*)?\s*\=\s*(?:([^;'"\s]+\'[\w-]*\'[^;\s]+)|(?:\"([^"]*)\")|([^;\s]*))/g;

exports.disposition = function (header) {

Expand Down
2 changes: 1 addition & 1 deletion test/esm.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ describe('import()', () => {

it('exposes all methods and classes as named imports', () => {

expect(Object.keys(Content)).to.equal([
expect(Object.keys(Content)).to.include([
'default',
'disposition',
'type'
Expand Down
141 changes: 136 additions & 5 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -95,22 +95,22 @@ describe('type()', () => {

it('errors on missing header', () => {

expect(() => Content.type()).to.throw();
expect(() => Content.type()).to.throw('Invalid content-type header');
});

it('errors on invalid header', () => {

expect(() => Content.type('application; some')).to.throw();
expect(() => Content.type('application; some')).to.throw('Invalid content-type header');
});

it('errors on multipart missing boundary', () => {

expect(() => Content.type('multipart/form-data')).to.throw();
expect(() => Content.type('multipart/form-data')).to.throw('Invalid content-type header: multipart missing boundary');
});

it('errors on multipart missing boundary (other params)', () => {

expect(() => Content.type('multipart/form-data; some=thing')).to.throw();
expect(() => Content.type('multipart/form-data; some=thing')).to.throw('Invalid content-type header: multipart missing boundary');
});

it('handles multiple boundary params', () => {
Expand All @@ -128,6 +128,19 @@ describe('type()', () => {
Content.type(header);
expect(Date.now() - now).to.be.below(100);
});

it('handles trailing newline in content-type without backtracking', () => {

const header = 'a/b' + 'c'.repeat(50000) + '\n';
const now = Date.now();
expect(() => Content.type(header)).to.throw('Invalid content-type header');
expect(Date.now() - now).to.be.below(100);
});

it('errors on content-type with embedded newline', () => {

expect(() => Content.type('application/json\n; charset=utf-8')).to.throw('Invalid content-type header');
});
});

describe('disposition()', () => {
Expand Down Expand Up @@ -164,7 +177,15 @@ describe('disposition()', () => {

const now = Date.now();
const header = `form-data; x; ${new Array(5000).join(' ')};`;
expect(() => Content.disposition(header)).to.throw();
expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header missing name parameter');
expect(Date.now() - now).to.be.below(100);
});

it('handles trailing newline in content-disposition without backtracking', () => {

const header = 'form-data;' + ' '.repeat(50000) + '\n';
const now = Date.now();
expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format');
expect(Date.now() - now).to.be.below(100);
});

Expand Down Expand Up @@ -220,4 +241,114 @@ describe('disposition()', () => {
const header = 'form-data; name="__proto__"; filename=file.jpg';
expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header format includes invalid parameters');
});

it('handles ReDoS exploit (polynomial backtracking via crafted commas and spaces)', () => {

const N = 4000;
const header = 'form-data;' + '=' + ','.repeat(N) + '"=' + ' '.repeat(N) + '= "';
const now = Date.now();
expect(() => Content.disposition(header)).to.throw('Invalid content-disposition header missing name parameter');
expect(Date.now() - now).to.be.below(100);
});

it('handles large number of spaces around equals', () => {

const now = Date.now();
const header = `form-data; name${' '.repeat(10000)}=${' '.repeat(10000)}file`;
Content.disposition(header);
expect(Date.now() - now).to.be.below(100);
});

it('handles large number of parameters', () => {

const params = Array.from({ length: 1000 }, (_, i) => `p${i}=v${i}`).join('; ');
const header = `form-data; name="file"; ${params}`;
const now = Date.now();
Content.disposition(header);
expect(Date.now() - now).to.be.below(100);
});

it('parses header with spaces around equals', () => {

const header = 'form-data; name = "file" ; filename = file.jpg';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'file.jpg' });
});

it('parses header with empty token value', () => {

const header = 'form-data; name="file"; filename=';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: '' });
});

it('parses header with multiple parameters', () => {

const header = 'form-data; name="file"; filename="test.jpg"; size=1234';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg', size: '1234' });
});

it('parses header with ext-value and regular params', () => {

const header = 'form-data; name="file"; filename*=utf-8\'en\'my%20file.jpg; size=999';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'my file.jpg', size: '999' });
});

it('parses header with no trailing semicolon', () => {

const header = 'form-data; name="file"; filename=test.jpg';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' });
});

it('parses header with trailing semicolon', () => {

const header = 'form-data; name="file"; filename=test.jpg;';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' });
});

it('parses header with extra whitespace between params', () => {

const header = 'form-data; name="file" ; filename=test.jpg ; size=100';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg', size: '100' });
});

it('parses header with hyphenated parameter name', () => {

const header = 'form-data; name="file"; file-name=test.jpg';
expect(Content.disposition(header)).to.equal({ name: 'file', 'file-name': 'test.jpg' });
});

it('parses header with dotted parameter name', () => {

const header = 'form-data; name="file"; file.name=test.jpg';
expect(Content.disposition(header)).to.equal({ name: 'file', 'file.name': 'test.jpg' });
});

it('parses header with ext-value without language tag', () => {

const header = 'form-data; name="file"; filename*=utf-8\'\'my%20file.jpg';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'my file.jpg' });
});

it('parses params even when followed by non-param garbage', () => {

const header = 'form-data; name="file"; filename=test.jpg GARBAGE';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' });
});

it('parses params when a key without value follows', () => {

const header = 'form-data; name="file"; filename=test.jpg; notaparam';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' });
});

it('parses header with trailing whitespace after last param', () => {

const header = 'form-data; name="file"; filename=test.jpg ';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' });
});

it('parses header with trailing whitespace after last quoted param', () => {

const header = 'form-data; name="file"; filename="test.jpg" ';
expect(Content.disposition(header)).to.equal({ name: 'file', filename: 'test.jpg' });
});
});
Loading