Repository navigation
chore: Updated content-type header parsing - #6414
Conversation
c6e17ba to
817bc34
Compare
gurgunday
left a comment
There was a problem hiding this comment.
I think this is hard to land in v5
Also, we should check how much it affects the performance
Unfortunately we removed the benchmark CI, someone should run it locally (I'll do it when I review)
|
It like a light weight version of content-type parser? How the performance after the change? |
93f288a to
b620bfc
Compare
In our code base, "content-type parser" refers to the thing that collects and manages the functions used to parse HTTP POST bodies. Prior to this PR, the indicator used to denote what format those POST bodies is in was inspected in various ways that are very fragile. This PR introduces a dedicated identifier parser.
I don't know. Likely slower than the previous fragile checks, but likely not the slowest possible. Unfortunately, proper handling of the |
b620bfc to
bd67b41
Compare
mcollina
left a comment
There was a problem hiding this comment.
Are you sure that there are no content types without a /? I think a user could create one like that, and I feel we should kind of support it.
Please review https://httpwg.org/specs/rfc9110.html#media.type The required format is |
|
@mcollina I have written the benchmark below to test the performance of splitting out only the Benchmark'use strict'
const assert = require('node:assert')
const { Suite } = require('bench-node')
const suite = new Suite({
reporter
})
const basicMediaType = 'application/json'
const paramsMediaType = 'application/json; charset=utf-8; foo=bar'
suite.add('iife split (basic)', () => {
let parts = (() => {
const [first, ...rest] = basicMediaType.split('/')
if (rest.length === 0) return [first]
return [first, rest.join('/')]
})()
assert.equal(parts[0], 'application')
assert.equal(parts[1], 'json')
})
suite.add('iife split (params)', () => {
let parts = (() => {
const [first, ...rest] = paramsMediaType.split('/')
if (rest.length === 0) return [first]
return [first, rest.join('/')]
})()
const type = parts[0]
parts = parts[1].split(';')
const subtype = parts[0]
assert.equal(type, 'application')
assert.equal(subtype, 'json')
})
suite.add('long split (basic)', () => {
let parts = basicMediaType.split('/')
assert.equal(parts[0], 'application')
assert.equal(parts[1], 'json')
})
suite.add('long split (params)', () => {
let parts = paramsMediaType.split('/')
const type = parts[0]
parts = parts[1].split(';')
const subtype = parts[0]
assert.equal(type, 'application')
assert.equal(subtype, 'json')
})
const tokensReg = /^([\w!#$%&'*+.^`|~-]+)\/([\w!#$%&'*+.^`|~-]+)\s*;?/
suite.add('regex split (basic)', () => {
const matches = tokensReg.exec(basicMediaType)
const type = matches[1]
const subtype = matches[2]
assert.equal(type, 'application')
assert.equal(subtype, 'json')
})
suite.add('regex split (params)', () => {
const matches = tokensReg.exec(paramsMediaType)
const type = matches[1]
const subtype = matches[2]
assert.equal(type, 'application')
assert.equal(subtype, 'json')
})
suite.run()
const { format } = new Intl.NumberFormat('en-US', {
useGrouping: true
})
function reporter (results) {
for (const result of results) {
const ops = format(result.opsSec)
console.log(`${result.name}: ${ops} ops/sec`)
}
} |
|
I'll be getting back to this soon. |
4523a2b to
126463b
Compare
|
I've analyzed the blocker regarding the performance vs. accuracy of the header parsing. Since Regex is proving difficult for strictness and split has overhead, the most performant way to handle this in V8 is usually a direct indexOf slice. Here is a proposed solution that fixes the vulnerability (adding the missing else check) and includes a high-performance parser that passes the tests without Regex complexity.
The Logic Fix (Inside validate) This ensures we don't "Fail Open" if the content type is missing from the schema map. This approach should satisfy the 415 test expectation and resolve the performance regression. |
23c9425 to
7bbaad3
Compare
Co-authored-by: Manuel Spigolon <behemoth89@gmail.com> Signed-off-by: James Sumners <321201+jsumners@users.noreply.github.com>
|
|
Whats the appropriate workaround/approach to take when a client is serving up invalid headers? |
It is not clear to me fastify.post('/', async function(request, reply) {
if(request.mediaType == undefined) {
// You can use hook to do the same thing.
throw Error('Invalid Content-Type')
} else {
reply.send({ ok: true })
}
})For |
|
I'm pretty confused, and I think maybe this isn't quite right yet. RFC 9110 states:
In a Fastify service that receives webhooks from Jira, for example, we observe Best I can tell, a request ends up with duplicate We do not appear to enable Previously, for After this change, however, I understand that Is @climba03003, if my understanding is correct, checking My main concerns are:
|
Keep malformed content types rejected by default while allowing an explicitly registered RegExp parser to recover known external values. Keep request handling independent of media type parsing so validation, normalization, and recovery remain encapsulated in the content type parser. Preserve existing catch-all and missing-route behavior. Followup to fastify#6414
|
I sketched out my idea here: #6918 @jsumners I think this actually still aligns with the intention here, while also making this less of a breaking change. Essentially it restores the ability to use a regxp |
"MAY" is not "MUST". As noted elsewhere, the Fastify project opts to strictly adhere to the RFCs.
This sounds like you are able to handle these absolutely incorrect header values.
My position is: yes, the The simple fact of the matter is that invalid content-type headers are constant source of "security" reports. My position will always be behind the strict parser, if for no reason other than to get those useless reports to stop. |
Yes of course I can accommodate it. But I had to change in a non-breaking update. My point is that you can still tighten the validation without moving where that happens. This change was also a breaking lifecycle policy change. #6918 effectively keeps your change but avoids the breaking change which doesn't seem nessisary for any of the goals here unless I'm mistaken. |
|
My goals were accomplished. The content-type parser was permitting invalid headers to pass through and/or jump to incorrect logic. This PR fixed those bugs. I am against any change that restores the previous behavior. |
|
The current implementation still lets some invalid headers through to addContentTypeParser. (Do you want test more cases?) Is the intention to strictly gate all of those out now? Clarifying intention here would help me understand what's acceptable here. |
|
I have seen you mention a case where a type+subtype is appended to the parameters list. That's just a bad parameters list and this feature explicitly opts to best effort those at this time. They are not used internally except to match incoming requests to the correct parser. A patch that rejects this malformed content in an efficient manner would be acceptable. |
|
Just chatted with @mcollina on a call about this and have a better understanding of what the issue/constraints are.
Will poke around if I can come up with something, thank you for the guidance. |
|
Small docs PR: #6925 Explains the new behavior and hazard. |
|
This PR: #6926 Makes strict validation prior to consulting |
|
This PR: #6927 Adds a different/alternative/complementary escape hatch allowing callers to customize/extend/replace the ContentType parser. |
|
I sent some time trying to work through understand what is needed here wrt strict RFC 9110 validation, but also the lax trailer parsing, and normalization recovery that remains. Looks like there might be some whitespace normalization leaks as these seems to leak through. Currently |
|
I honestly can't remember the full details around it. But my vague recollection is that parts of the codebase allowed for empty values while needing to distinguish such a value from an invalid header. Improvements are welcome. |
Document the request lifecycle boundary introduced by #6414: - malformed `Content-Type` values are rejected before body parser selection - string, RegExp, and catch-all parsers cannot recover invalid values - an `onRequest` hook can narrowly canonicalize known external input - repair must happen before `request.mediaType` is accessed and cached - broad recovery can undermine parser selection and `schema.body.content` guarantees The example intentionally handles one exact malformed value and leaves every unrelated value under Fastify strict validation. <!-- Thank you for your pull request. Please provide a description above and review the requirements below. Bug fixes and new features should include tests and possibly benchmarks. Contributors guide: https://github.com/fastify/fastify/blob/main/CONTRIBUTING.md By making a contribution to this project, I certify that: * (a) The contribution was created in whole or in part by me and I have the right to submit it under the open source license indicated in the file; or * (b) The contribution is based upon previous work that, to the best of my knowledge, is covered under an appropriate open source license and I have the right under that license to submit that work with modifications, whether created in whole or in part by me, under the same open source license (unless I am permitted to submit under a different license), as indicated in the file; or * (c) The contribution was provided directly to me by some other person who certified (a), (b) or (c) and I have not modified it. * (d) I understand and agree that this project and the contribution are public and that a record of the contribution (including all personal information I submit with it, including my sign-off) is maintained indefinitely and may be redistributed consistent with this project or the open source license(s) involved. --> #### Checklist - [x] run `npm run test && npm run benchmark --if-present` - [x] tests and/or benchmarks are included - [x] documentation is changed or added - [x] commit message and code follows the [Developer's Certification of Origin](https://github.com/fastify/.github/blob/main/CONTRIBUTING.md#developers-certificate-of-origin-11) and the [Code of conduct](https://github.com/fastify/.github/blob/main/CODE_OF_CONDUCT.md) --------- Signed-off-by: Bret Comnes <bret@comnes.org> Signed-off-by: Bret Comnes <166301+bcomnes@users.noreply.github.com> Co-authored-by: James Sumners <321201+jsumners@users.noreply.github.com>
Resolves #6332.
This is a follow-on from #6303. This introduces a more robust content-type header value parsing algorithm.
IMPORTANT: this results in much stricter content-type handling. Prior to this PR, we were accepting, and handling, headers like
content-type: a-simple-string. That is not valid. There must be a type and a subtype present. So a "valid" form would becontent-type: a-simple-string/foo.This change also enforces normalization of the content-type value. For example, if the request has
application/json; charset=utf-8as the value, it will be normalized toapplication/json; charset="utf-8".