Opened 9 months ago

Last modified 19 hours 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: yes 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 (3)

comment:1 by Jacob Walls, 9 months ago

Keywords: HTTP_ACCEPT accept added
Resolution: needsinfo
Status: assignedclosed

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, 9 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, 19 hours ago

Cc: Jake Howard added
Keywords: Content-Type Cache-Control not-security added
Resolution: needsinfo
Status: closednew
Summary: HttpRequest.accepted_types incorrectly splits Accept header on commas inside quoted parameter valuesHTTP headers are incorrectly split on commas inside quoted parameter values
Triage Stage: UnreviewedAccepted

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.

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