Opened 10 months ago

Last modified 3 weeks ago

#36833 new Bug

HTTP headers are incorrectly split on commas inside quoted parameter values

Reported by: Naveed Qadir Owned by: Naveed Qadir
Component: HTTP handling Version: dev
Severity: Normal Keywords: HTTP_ACCEPT, accept, Content-Type, Cache-Control, not-security
Cc: Naveed Qadir, Jake Howard Triage Stage: Accepted
Has patch: no Needs documentation: no
Needs tests: no Patch needs improvement: no
Easy pickings: no UI/UX: no

Description

The accepted_types property in HttpRequest uses str.split(",") to parse the Accept header, which incorrectly splits on commas that appear inside quoted parameter values.

Example

# Accept header with quoted parameter containing comma
header = 'text/plain; param="a,b", application/json'

# Current behavior (WRONG):
header.split(",")
# Returns: ['text/plain; param="a', 'b"', ' application/json']
# 3 parts - comma inside quotes was incorrectly treated as separator

# Expected behavior (per RFC 7231):
# Should return 2 media types:
# 1. text/plain; param="a,b"
# 2. application/json

RFC Reference

RFC 7231 Section 5.3.2 specifies that media-type parameters can contain quoted-string values, and RFC 7230 Section 3.2.6 allows commas within quoted strings.

Proposed Fix

Add a split_header_words() helper function to django/utils/http.py that splits on commas while respecting quoted strings, similar to how _parseparam() handles semicolons.

A patch with tests is available.

Change History (4)

comment:1 by Jacob Walls, 10 months ago

Keywords: HTTP_ACCEPT accept added
Resolution: → needsinfo
Status: assigned → closed

Do you have an example of real-world HTTP traffic that sends params like that for the accept header?

Looking at the ​provided patch, this is too much complexity for the benefit. I'd also expect to block this on a resolution for #35440, with the hope that we can leverage some existing pattern for param parsing using python's stdlib.

in reply to:  1 comment:2 by Naveed Qadir, 10 months ago

Thanks for the feedback and for pointing me to #35440 — I agree that reusing a stdlib-based approach would be preferable if we can do so without performance regressions.
Regarding real-world usage: I’m not aware of common browsers or clients emitting Accept headers with quoted parameters containing commas or escaped quotes. The change was motivated by spec-permitted behavior and to avoid incorrect parsing when such headers do appear, but I agree this is rare in practice.
Given the complexity concerns and the direction of #35440, I’m happy to defer this and continue the discussion there.
Replying to Jacob Walls:

Do you have an example of real-world HTTP traffic that sends params like that for the accept header?

Looking at the ​provided patch, this is too much complexity for the benefit. I'd also expect to block this on a resolution for #35440, with the hope that we can leverage some existing pattern for param parsing using python's stdlib.

comment:3 by Jacob Walls, 3 weeks ago

Cc: Jake Howard added
Keywords: Content-Type Cache-Control not-security added
Resolution: needsinfo
Status: closed → new
Summary: HttpRequest.accepted_types incorrectly splits Accept header on commas inside quoted parameter values → HTTP headers are incorrectly split on commas inside quoted parameter values
Triage Stage: Unreviewed → Accepted

Reopening and accepting and broadening the scope to cover other header parsing use cases in Django such as Cache-Control, Vary, Content-Type, etc.


The Security Team received several duplicate reports about hypothetical extension directives for the Cache-Control header that could theoretically bypass the safety check in UpdateCacheMiddleware.process_response for CVE-2026-35193 that currently looks like:

        cache_control_parts = set(
            split_directive_names(response.get("Cache-Control", ""))
        )
...
        if request.headers.get("Authorization") and "public" not in cache_control_parts:
            patch_vary_headers(response, ("Authorization",))

To reproduce with the relevant helpers, see:

>>> list(split_directive_names('example="alpha, public, omega"'))
['example', 'public', 'omega"']
>>> list(split_header_value('example="alpha, public, omega"'))
['example="alpha', 'public', 'omega"']

That's the same class of bug as this ticket for Accept.

Jake Howard triaged:

This is obviously not right. It looks like the root cause is in split_header_value, which is also used when parsing ETag and Vary headers.

The docstring states that split_header_value shouldn't be used for headers where quoted strings are acceptable, however according to the RFC, Cache-Control does support quoted strings as token values.

I say "theoretically" in this sense, as put by Jake Howard:

None of the standard Cache-Control headers support "public" in the value portion, so any behaviour which would trigger this would be from custom logic, where we have less control.

We solved a very similar issue for substrings publicly in 142b881cecaddc334cabec139e701c0e4b9798da and b461519bf5973d7fc149560d2f99acdba71a437d, so this class of issues was not treated as a security issue, either.

Still, in an effort to be more correct and stem the tide of duplicate security reports, we can fix this.

Jake also added:

I think the solution is to extract some of the logic from parse_header_parameters, which supports quoted strings and parameters correctly, and reuse it.

As an aside, Vary definitely doesn't accept quoted strings (bare tokens only), and ETag quotes have meaning, so only accepts a single quoted string. Their behaviour should still be correct.

I agree -- following comment:1, I'll reopen #35440 and block this on that.

comment:4 by blighj, 3 weeks ago

Has patch: unset
Note: See TracTickets for help on using tickets.
Back to Top