T3MP3ST public launch build

This commit is contained in:
t3mp3st contributors committed 2026-07-03 10:57:20 -04:00
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."}