T3MP3ST public launch build
This commit is contained in:
1 parent
972247b881
commit
184c7d4e59
1589 files changed
+57150
-506
No files matched your search
@@ -0,0 +1,180 @@
|
||||
--- 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');
|
||||
+ });
|
||||
});
|
||||
@@ -0,0 +1 @@
|
||||
{"ghsa": "GHSA-36hh-x5p5-jgc8", "cve": "CVE-2026-44974", "repo": "hapijs/content", "severity": "high", "cwes": ["CWE-436"], "summary": "@hapi/content header parser has a parameter smuggling issue that allows upload-filter bypass via duplicate parameters", "fix_commit": "3850079550c191d25e3643dc82a6d61144db8c2f", "parent_commit": "e4233cee05535716991d02d55f82205514b10ae8", "vuln_files": ["lib/index.js"], "vuln_lines": {"lib/index.js": [18, 19, 121]}, "published": "2026-05-27T00:37:20Z", "source_files": 1, "scope_dirs": ["lib"]}
|
||||
@@ -0,0 +1,145 @@
|
||||
'use strict';
|
||||
|
||||
const Boom = require('@hapi/boom');
|
||||
|
||||
|
||||
const internals = {};
|
||||
|
||||
|
||||
/*
|
||||
RFC 7231 Section 3.1.1.1
|
||||
|
||||
media-type = type "/" subtype *( OWS ";" OWS parameter )
|
||||
type = token
|
||||
subtype = token
|
||||
parameter = token "=" ( token / quoted-string )
|
||||
*/
|
||||
|
||||
// 1: type/subtype 2: params
|
||||
internals.contentTypeRegex = /^([^\/\s]+\/[^\s;]+)([ \t;][^\r\n]*)?$/;
|
||||
|
||||
// 1: "b" 2: b
|
||||
internals.charsetParamRegex = /;\s*charset=(?:"([^"]+)"|([^;"\s]+))/i;
|
||||
|
||||
// 1: "b" 2: b
|
||||
internals.boundaryParamRegex = /;\s*boundary=(?:"([^"]+)"|([^;"\s]+))/i;
|
||||
|
||||
|
||||
exports.type = function (header) {
|
||||
|
||||
if (!header) {
|
||||
throw Boom.badRequest('Invalid content-type header');
|
||||
}
|
||||
|
||||
const match = header.match(internals.contentTypeRegex);
|
||||
if (!match) {
|
||||
throw Boom.badRequest('Invalid content-type header');
|
||||
}
|
||||
|
||||
const result = {
|
||||
mime: match[1].toLowerCase()
|
||||
};
|
||||
|
||||
const params = match[2];
|
||||
if (params) {
|
||||
const param = params.match(internals.charsetParamRegex);
|
||||
if (param) {
|
||||
result.charset = (param[1] || param[2]).toLowerCase();
|
||||
}
|
||||
}
|
||||
|
||||
if (result.mime.indexOf('multipart/') === 0) {
|
||||
if (params) {
|
||||
const param = params.match(internals.boundaryParamRegex);
|
||||
if (param) {
|
||||
result.boundary = param[1] || param[2];
|
||||
}
|
||||
}
|
||||
|
||||
if (!result.boundary) {
|
||||
throw Boom.badRequest('Invalid content-type header: multipart missing boundary');
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
|
||||
/*
|
||||
RFC 6266 Section 4.1 (http://tools.ietf.org/html/rfc6266#section-4.1)
|
||||
|
||||
content-disposition = "Content-Disposition" ":" disposition-type *( ";" disposition-parm )
|
||||
disposition-type = "inline" | "attachment" | token ; case-insensitive
|
||||
disposition-parm = filename-parm | token [ "*" ] "=" ( token | quoted-string | ext-value) ; ext-value defined in [RFC5987], Section 3.2
|
||||
|
||||
Content-Disposition header field values with multiple instances of the same parameter name are invalid.
|
||||
|
||||
Note that due to the rules for implied linear whitespace (Section 2.1 of [RFC2616]), OPTIONAL whitespace
|
||||
can appear between words (token or quoted-string) and separator characters.
|
||||
|
||||
Furthermore, note that the format used for ext-value allows specifying a natural language (e.g., "en"); this is of limited use
|
||||
for filenames and is likely to be ignored by recipients.
|
||||
*/
|
||||
|
||||
|
||||
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]*))/g;
|
||||
|
||||
exports.disposition = function (header) {
|
||||
|
||||
if (!header) {
|
||||
throw Boom.badRequest('Missing content-disposition header');
|
||||
}
|
||||
|
||||
const match = header.match(internals.contentDispositionRegex);
|
||||
if (!match) {
|
||||
throw Boom.badRequest('Invalid content-disposition header format');
|
||||
}
|
||||
|
||||
const parameters = match[1];
|
||||
if (!parameters) {
|
||||
throw Boom.badRequest('Invalid content-disposition header missing parameters');
|
||||
}
|
||||
|
||||
const result = {};
|
||||
parameters.replace(internals.contentDispositionParamRegex, ($0, $1, $2, $3, $4, $5) => {
|
||||
|
||||
if ($1 === '__proto__') {
|
||||
throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters');
|
||||
}
|
||||
|
||||
let value;
|
||||
|
||||
if ($2) {
|
||||
if (!$3) {
|
||||
throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters');
|
||||
}
|
||||
|
||||
try {
|
||||
value = decodeURIComponent($3.split('\'')[2]);
|
||||
}
|
||||
catch (err) {
|
||||
throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters');
|
||||
}
|
||||
}
|
||||
else {
|
||||
value = $4 || $5 || '';
|
||||
}
|
||||
|
||||
if ($1 === 'name' &&
|
||||
value === '__proto__') {
|
||||
|
||||
throw Boom.badRequest('Invalid content-disposition header format includes invalid parameters');
|
||||
}
|
||||
|
||||
result[$1] = value;
|
||||
});
|
||||
|
||||
if (!result.name) {
|
||||
throw Boom.badRequest('Invalid content-disposition header missing name parameter');
|
||||
}
|
||||
|
||||
return result;
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
{"id": "GHSA-36hh-x5p5-jgc8", "repo": "hapijs/content", "scope": ["lib"], "files": 1, "objective": "Audit this real-world source tree for the single most serious exploitable security vulnerability."}
|
||||
Reference in new issue
Block a user