Skip to content

fix(content-type): escape quoted-pairs when serializing parameters - #7022

Open
Khizarc wants to merge 1 commit into
fastify:mainfrom
Khizarc:fix/content-type-quoted-pair-serialization
Open

Khizarc wants to merge 1 commit into
fastify:mainfrom
Khizarc:fix/content-type-quoted-pair-serialization

Conversation

@Khizarc

@Khizarc Khizarc commented Sep 12, 2026

Copy link
Copy Markdown

Checklist

  • run npm run test and npm run benchmark
  • tests and/or benchmarks are included
  • documentation is changed or added — N/A, no public API change
  • commit message and code follow the Code of conduct

No open issue — found by auditing lib/content-type.js.

The bug

The constructor unescapes quoted-pairs, as RFC 9110 §5.6.4 requires of a recipient:

if (value.indexOf('\\') !== -1) {
  value = value.replace(quotedPairReg, '$1')
}

So a stored parameter value can contain a DQUOTE or a backslash. toString() then wrapped it in DQUOTEs verbatim:

parameters.push(`${key}="${value}"`)

which produces a header that no longer parses back to the same value:

input parsed toString() re-parsed
p="a\"b" a"b p="a"b" a
p="a\\b" a\b p="a\b" ab

Why it is not only cosmetic

toString() is the normalized key that custom parsers are registered and looked up under — ContentTypeParser.prototype.add stores ct.toString(), and getParser looks up contentType.toString() for the incoming request. Because the escaping is dropped, two different content types collapse onto one key:

new ContentType('text/plain; p="a\\"; q=\\"b"')  // 1 parameter: p === 'a"; q="b'
new ContentType('text/plain; p="a"; q="b"')      // 2 parameters: p === 'a', q === 'b'
// both serialize to:  text/plain; p="a"; q="b"

A parser registered for the first therefore runs for requests carrying the second. There is a test for exactly this through inject.

The fix

Precede each DQUOTE and backslash with a backslash when serializing, which is what RFC 9110 §5.6.4 requires of a sender. A value containing neither octet is unchanged, so the existing toString() assertions in test/content-type.test.js are untouched.

I deliberately did not also stop quoting values that are bare tokens (charset=utf-8 still serializes as charset="utf-8"). That round-trips correctly today, and changing it would alter every existing parser key — a separate question from this bug.

Verification

  • 4 new tests fail before the change, pass after
  • npm run unit: 2345 tests, 0 failures
  • npm run lint clean, npm run test:types clean (1282 assertions)

The constructor unescapes quoted-pairs per RFC 9110 §5.6.4, so a parameter
value in `#parameters` can hold a DQUOTE or a backslash. `toString()` wrapped
that value in DQUOTEs verbatim, producing a header that no longer parses back
to the same value:

  input        text/plain; p="a\"b"     text/plain; p="a\\b"
  parsed       a"b                      a\b
  toString()   text/plain; p="a"b"      text/plain; p="a\b"
  re-parsed    a                        ab

That matters beyond serialization, because `toString()` is the key custom
parsers are registered and looked up under in content-type-parser.js. Two
different content types could normalize to one key:

  text/plain; p="a\"; q=\"b"   -> one parameter, p == 'a"; q="b'
  text/plain; p="a"; q="b"     -> two parameters, p == 'a', q == 'b'

Both serialized to `text/plain; p="a"; q="b"`, so a parser registered for the
first ran for requests carrying the second.

Precede each DQUOTE and backslash with a backslash on the way out, which is
what RFC 9110 §5.6.4 requires of a sender. Values without either octet are
unchanged.

Signed-off-by: Khizar <Khizarchaudhryy@gmail.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

@Asadshah7950 Asadshah7950 left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technical Review

This is a clean and spec-compliant fix adhering to RFC 9110 §5.6.4.

Key Observations:

  1. RFC 9110 §5.6.4 Compliance: Correctly identifies " (DQUOTE) and \ (backslash) as the only two characters that cannot appear literally inside a quoted-string and must be escaped as quoted-pair by a sender.
  2. Parser Registration & Security: Directly resolves the key collision issue where an unescaped parameter value containing "; q=" could synthesize additional parameters upon toString(), causing unrelated requests to alias into custom parsers registered via addContentTypeParser.
  3. Round-Trip Fidelity: Guarantees that new ContentType(ct.toString()) reconstructs the exact original parameter key-value pairs without loss or truncation.
  4. Test Coverage: The test additions in test/content-type.test.js are thorough, verifying DQUOTE escaping, backslash escaping, parameter injection prevention, and runtime parser isolation through app.inject().

LGTM!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants