@hapi/content header parser has a parameter smuggling issue that allows upload-filter bypass via duplicate parameters
Description
Impact
The two parsers resolved duplicates inconsistently and silently: - Content.disposition() retained the last occurrence of each parameter. - Content.type() retained the first occurrence of charset and boundary.
Either behavior creates a parameter-smuggling primitive when another component in the request-processing chain (a WAF, reverse proxy, security filter, or alternate parser) resolves duplicates the opposite way. The primary attack vector is upload filename allowlist bypass:
Content-Disposition: form-data; name="file"; filename="safe.txt"; filename="shell.php"
Patches
The issue has been patched in 6.0.2.
Workarounds
Pre or post validate headers looking for duplicates.
### Resources - RFC 6266 §4.1 — Content-Disposition syntax - RFC 7231 §3.1.1.1 — Content-Type syntax - RFC 7230 §3.2.6 — token character set
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
The @hapi/content library resolves duplicate HTTP header parameters inconsistently, enabling parameter smuggling to bypass upload filename allowlist filters.
Vulnerability
The @hapi/content library (versions prior to 6.0.2) parses Content-Disposition and Content-Type headers with inconsistent duplicate parameter resolution: Content.disposition() retains the last occurrence of each parameter, while Content.type() retains the first occurrence of charset and boundary. This discrepancy creates a parameter-smuggling primitive when another component in the request-processing chain (e.g., a WAF, reverse proxy, security filter, or alternate parser) resolves duplicates in the opposite manner. The affected versions are all prior to 6.0.2 [1], [3].
Exploitation
An attacker needs no special privileges and can exploit this by sending a crafted HTTP request with duplicate header parameters. For the primary attack vector—upload filename allowlist bypass—the attacker provides a Content-Disposition header such as form-data; name="file"; filename="safe.txt"; filename="shell.php". The security filter may evaluate the first occurrence ("safe.txt") and allow the request, while the @hapi/content parser uses the last occurrence ("shell.php") for server-side processing, enabling an upload of a malicious file [1], [3].
Impact
Successful exploitation allows an attacker to bypass filename allowlist validation and upload files with arbitrary extensions (e.g., .php), potentially leading to remote code execution or other severe consequences depending on how the uploaded file is handled by the application. The vulnerability directly undermines confidentiality, integrity, and availability when combined with downstream processing [1], [3].
Mitigation
The issue has been patched in version 6.0.2 of @hapi/content. Users should upgrade immediately to this version or later. As a workaround, applications can pre- or post-validate headers to reject any duplicate parameters. There is no indication that this CVE is listed in CISA's Known Exploited Vulnerabilities (KEV) catalog [1], [2], [3].
AI Insight generated on May 27, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1- Range: < 6.0.2
Patches
13850079550c1fix: error on duplicate parameters
2 files changed · +105 −5
lib/index.js+29 −3 modified@@ -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+76 −2 modified@@ -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'); + }); });
Vulnerability mechanics
Root cause
"Inconsistent duplicate-parameter resolution between Content.disposition() (last-wins) and Content.type() (first-wins) creates a parameter-smuggling primitive."
Attack vector
An attacker sends an HTTP request containing a `Content-Disposition` or `Content-Type` header with duplicate parameter keys (e.g., `filename="safe.txt"; filename="shell.php"`). Prior to the patch, `Content.disposition()` silently kept the last occurrence of each parameter while `Content.type()` kept the first occurrence of `charset` and `boundary`. This inconsistency creates a parameter-smuggling primitive: when another component in the request-processing chain (a WAF, reverse proxy, security filter, or alternate parser) resolves duplicates the opposite way, the attacker can bypass filename allowlists or other security checks. The primary attack vector is upload filename allowlist bypass via `Content-Disposition: form-data; name="file"; filename="safe.txt"; filename="shell.php"` [ref_id=1].
What the fix does
The patch adds explicit duplicate-parameter detection in both `Content.type()` and `Content.disposition()` [patch_id=2595956]. For `Content.type()`, after matching `charset` or `boundary`, the code tests whether the same regex matches again in the remainder of the parameter string and throws an error if so. For `Content.disposition()`, the patch tracks seen parameter names in a `result` object and a separate `extKeys` set; if a plain parameter name is already defined and no ext-value override is in play, it throws `'Invalid content-disposition header format includes invalid parameters'`. The RFC 6266 §4.3 ext-value override pattern (regular value followed by `filename*`) is still permitted, but duplicate plain parameters or duplicate ext-value parameters are now rejected.
Preconditions
- networkAttacker must be able to send HTTP requests with crafted Content-Disposition or Content-Type headers to an application using the hapijs/content library
- configAnother component in the request-processing chain (WAF, reverse proxy, security filter, or alternate parser) must resolve duplicate parameters in the opposite order from the vulnerable library
Generated on May 27, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
3News mentions
0No linked articles in our index yet.