Repository navigation
Conversation
Use the WHATWG MIME parsing algorithm to recover usable media types from malformed parameter syntax. Preserve parsed parameters for content type parser matching and reply charset handling. Signed-off-by: Bret Comnes <bret@comnes.org>
climba03003
left a comment
There was a problem hiding this comment.
From what I see from RFC9110, it is increasing the security risk for the server and encourage to provide a trigger to disable the sniffing. I actually against using the WHATWG Media Type Sniffing, the intermediate server or web browser should send a proper and valid Content-Type header.
In practice, resource owners do not always properly configure their origin server to provide the correct Content-Type for a given representation. Some user agents examine the content and, in certain cases, override the received type (for example, see [Sniffing]). This "MIME sniffing" risks drawing incorrect conclusions about the data, which might expose the user to additional security risks (e.g., "privilege escalation"). Furthermore, distinct media types often share a common data format, differing only in how the data is intended to be processed, which is impossible to distinguish by inspecting the data alone. When sniffing is implemented, implementers are encouraged to provide a means for the user to disable it.
|
I'm sorry I don't quite understand how this is different than what we are doing already, other than following a slightly different algorithm. This doesn't add any content sniffing. It uses a cursor based parsing technique on the header instead of regex. |
| @@ -0,0 +1,231 @@ | |||
| 'use strict' | |||
There was a problem hiding this comment.
Please place the logic inside content-type.js no need to create multiple files serving a single purpose.
| }) | ||
|
|
||
| test('recovers invalid parameter syntax', (t) => { | ||
| const recoveredContentTypes = [ |
There was a problem hiding this comment.
Instead of relying human fixed value, I would battle test the result between node:util.
const { MIMEType } = require('node:util')
test('application/json; charset=utf-8,application/json', (t) => {
t.plan(4)
const ct = new ContentType('application/json; charset=utf-8,application/json')
const mt = new MIMEType('application/json; charset=utf-8,application/json')
t.assert.strictEqual(ct.type, mt.type)
t.assert.strictEqual(ct.subtype, mt.subtype)
t.assert.strictEqual(ct.mediaType, mt.essence)
t.assert.strictEqual(String(ct), String(mt))
})
How the performance between |
|
I honestly imagine similar performance degradation unless we take shortcuts on this implementation. Following the cursor based as per-spec is not the most performant unless tricks are done that can deviate from spec. I'd go on the line of having a performance RFC based implementation that following WHATWG, not opposed to that if optional or lazy loaded |
jsumners
left a comment
There was a problem hiding this comment.
Sorry, I understand the desire and justifications, but I am against backing out the work done to bring Fastify's content-type parser up to standard (RFC 9110). This is a constant source of "security" reports. A strict parser stops this.
For the record: I DO NOT consider WHATWG as being a "standard." We follow the IETF RFCs.
|
Going to rework this, see #6414 (comment) |
Follow-up to #6414.
I'm trying to find an approach that makes sense here:
addContentTypeParseras the place for application-specific handling, including exact string overrides for valid values and explicitly registeredRegExpparsers for invalid-header recovery, rather than forcing recovery into anonRequesthook (restored by Allow custom parsers to recover content types #6918).I suggest that Fastify parse
Content-Typevalues using the WHATWG MIME type parsing algorithm.This:
Fastify currently parses MIME parameters using a collection of regular expressions. That differs from the WHATWG recovery algorithm and can produce inconsistent results for malformed parameter syntax.
The observed webhook input appears to begin with
Content-Typebeing set more than once somewhere in the request path. An HTTP implementation or intermediary then combines those field lines with commas before Fastify receives them:becomes:
The sender, intermediary, or server layer responsible for introducing the duplicate is not necessarily known. Fastify receives only the combined value.
Adopting the WHATWG algorithm also resolves other parsing ambiguities, such as duplicate parameters:
The current Fastify parser lets the last parameter win:
WHATWG keeps the first parameter value:
This follows the algorithm's directive to set a parameter only when that name does not already exist, preventing a later duplicate from changing the interpretation.
If the second
Content-Typefield conflicts with the first, the current parser silently discards it:The current Fastify parser matches the valid parameter prefix and parses this as:
The trailing
,text/plainis discarded.WHATWG preserves the complete parameter value:
The MIME type remains
application/json, but the malformed charset value is no longer silently sanitized. This is nonsensical but more representative of reliantly, so in my opinion, preferable.This does not make comma-separated media types generally valid:
The comma is part of the subtype in that case, so the value remains invalid. String parsers cannot register invalid MIME types, but applications can explicitly recover known invalid values using a
RegExpcontent-type parser.By contrast, comma-containing parameter values remain valid under WHATWG and use the parser registered for their media type. Applications that need custom behavior for a specific valid value can register an exact string parser.
For example:
Fastify normalizes the registered value and the incoming header to the same MIME type representation, so this exact string parser takes precedence over the generic
application/jsonparser.The parser follows:
The implementation uses numeric cursor positions and lazy string slices, but preserves the specified parsing behavior.
Parameter names are validated as HTTP tokens before using native
String.prototype.toLowerCase(). Since valid HTTP tokens are ASCII, this is equivalent to the specification's ASCII-lowercase operation while avoiding Unicode case-folding surprises.Performance
Compared with
origin/mainata2f59056using Node.js 24.18.0.Each result is the paired median of 12 rounds with 500,000 constructions per round.
Benchmark code, run from the repository root:
application/jsonApplication/JSONPlain MIME types are within benchmark noise of the previous implementation. Parameterized and recovery cases are faster.
Validation
npm run lintnpm run unitResult: 2,310 tests passed, 4 skipped, 0 failed.
Checklist
npm run test && npm run benchmark --if-present