CVE-2026-114138
Django REST framework: Accept header with many large quoted parameters burns ~19s of CPU per request in content negotiation
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
Attack profile
How easy it is to exploit
- Attack vector
- Network
- Complexity
- Low
- Privileges needed
- None
- User interaction
- None
What an attacker can affect
- Confidentiality
- None
- Integrity
- None
- Availability
- High
Exploit likelihood
What each source reports
CVE List Score used
7.5
Django REST framework: Accept header with many large quoted parameters burns ~19s of CPU per request in content negotiation
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
- Record
CVE-2026-114138- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-407
- Updated
- 2026-11-02
EUVDEUVD-2026-107548
7.5
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
- Record
EUVD-2026-107548- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
GitHub AdvisoriesGHSA-33wh-fxxf-88vv
7.5
Django REST framework: Accept header with many large quoted parameters burns ~19s of CPU per request in content negotiation
Summary
DRF's content negotiation parses each Accept token several times per request and never limits the number of tokens or their total size. Each parse ends up in Django's parse_header_parameters, which takes quadratic time when a quoted parameter value is packed with semicolons. On my machine a single GET with a 780KB Accept header made of 96 such tokens kept one gunicorn worker busy for 18.87 seconds (Django 6.1, djangorestframework 3.18.0, gunicorn 26.0.0, Python 3.13, Docker on macOS). The header is syntactically valid, fits inside gunicorn's default header limits, and needs no authentication. Two concurrent requests, which is one per sync worker, made a default two worker deployment stop answering for my whole 40 second test window. It recovered half a second after I stopped sending.
Details
What happens
get_accept_list() splits the Accept header on commas with no cap on token count or total size, at rest_framework/negotiation.py:96-97.
For every token, select_renderer() calls media_type_matches() once per renderer, and each call builds fresh _MediaType objects for both sides, at rest_framework/negotiation.py:53-56 and rest_framework/utils/mediatypes.py:22-23. _MediaType.__init__ calls django.utils.http.parse_header_parameters unconditionally, at rest_framework/utils/mediatypes.py:47.
When nothing matches, DRF raises NotAcceptable (406). Rendering that error response also needs a renderer, so finalize_response() runs the entire negotiation a second time through perform_content_negotiation(force=True), at rest_framework/views.py:434-437.
With the default two renderers this comes to about six parses per Accept token per request. I confirmed the count by instrumenting _MediaType with a counter: six per token, three from the normal pass and three from the forced re-negotiation.
The quadratic part is on the Django side. parse_header_parameters splits parameters with _parseparam, which handles quoted values by recounting the quote characters from the start of the string for every candidate separator, the s.count('"', 0, end) calls in django/utils/http.py. A quoted value holding k semicolons costs roughly k rescans over k characters, so a single ~8KB value takes about 31ms per parse here.
Numbers from my runs, measured as utime+stime deltas of the server process:
one token, ~9KB: 230ms server CPU 24 tokens, 216KB: 5.07s on runserver, which has no per field limit 48 tokens, 432KB: 10.08s on runserver 96 tokens, 780KB: 18.87s under gunicorn defaults
Wall time matched CPU time in every run, so this is pure CPU work. RSS stayed flat at 57MB throughout, this is a time attack and not a memory one.
A few details that matter for reproducing it:
Django rejects a single media type string longer than 10,000 chars (MAX_HEADER_LENGTH), so each token has to stay just under that. Going over gives you a ValueError and a 500, which is a different and much weaker problem.
gunicorn defaults are limit_request_field_size 8190 and limit_request_fields 100, so the working shape is 96 separate Accept fields with one ~8.1KB token each. That puts the single request ceiling near 19s, below the default 30s worker timeout, so one request alone does not get a stock gunicorn worker killed by the arbiter. It does not need to. Each request pins one sync worker for its full duration, so on two workers, two held connections wedge the service.
order_by_precedence collects tokens into a set, so identical tokens collapse and the cost mostly disappears. Each token needs its own parameter name, x0=, x1=, x2= and so on.
Where a fix could go, either of these alone would be enough:
Parse each Accept token once per request and reuse the result in the matching loop and in the 406 re-negotiation.
Put an aggregate budget in get_accept_list(), say 8KB total or 64 tokens, and treat anything past it as not acceptable.
The quadratic behavior itself lives in Django's parser, but DRF is the one calling it six times per token with no…
- Record
GHSA-33wh-fxxf-88vv- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-407
- Updated
- 2026-11-02
NVD
7.5
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
- Record
CVE-2026-114138- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-407
- Updated
- 2026-11-02
OSV
7.5
Django REST framework: Accept header with many large quoted parameters burns ~19s of CPU per request in content negotiation
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
- Record
CVE-2026-114138- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-407
- Updated
- 2026-11-02
OSVDEBIAN-CVE-2026-114138
7.5
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
- Record
DEBIAN-CVE-2026-114138- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
Show 4 moreShow less
OSVGHSA-33wh-fxxf-88vv
7.5
Django REST framework: Accept header with many large quoted parameters burns ~19s of CPU per request in content negotiation
Summary
DRF's content negotiation parses each Accept token several times per request and never limits the number of tokens or their total size. Each parse ends up in Django's parse_header_parameters, which takes quadratic time when a quoted parameter value is packed with semicolons. On my machine a single GET with a 780KB Accept header made of 96 such tokens kept one gunicorn worker busy for 18.87 seconds (Django 6.1, djangorestframework 3.18.0, gunicorn 26.0.0, Python 3.13, Docker on macOS). The header is syntactically valid, fits inside gunicorn's default header limits, and needs no authentication. Two concurrent requests, which is one per sync worker, made a default two worker deployment stop answering for my whole 40 second test window. It recovered half a second after I stopped sending.
Details
What happens
get_accept_list() splits the Accept header on commas with no cap on token count or total size, at rest_framework/negotiation.py:96-97.
For every token, select_renderer() calls media_type_matches() once per renderer, and each call builds fresh _MediaType objects for both sides, at rest_framework/negotiation.py:53-56 and rest_framework/utils/mediatypes.py:22-23. _MediaType.__init__ calls django.utils.http.parse_header_parameters unconditionally, at rest_framework/utils/mediatypes.py:47.
When nothing matches, DRF raises NotAcceptable (406). Rendering that error response also needs a renderer, so finalize_response() runs the entire negotiation a second time through perform_content_negotiation(force=True), at rest_framework/views.py:434-437.
With the default two renderers this comes to about six parses per Accept token per request. I confirmed the count by instrumenting _MediaType with a counter: six per token, three from the normal pass and three from the forced re-negotiation.
The quadratic part is on the Django side. parse_header_parameters splits parameters with _parseparam, which handles quoted values by recounting the quote characters from the start of the string for every candidate separator, the s.count('"', 0, end) calls in django/utils/http.py. A quoted value holding k semicolons costs roughly k rescans over k characters, so a single ~8KB value takes about 31ms per parse here.
Numbers from my runs, measured as utime+stime deltas of the server process:
one token, ~9KB: 230ms server CPU 24 tokens, 216KB: 5.07s on runserver, which has no per field limit 48 tokens, 432KB: 10.08s on runserver 96 tokens, 780KB: 18.87s under gunicorn defaults
Wall time matched CPU time in every run, so this is pure CPU work. RSS stayed flat at 57MB throughout, this is a time attack and not a memory one.
A few details that matter for reproducing it:
Django rejects a single media type string longer than 10,000 chars (MAX_HEADER_LENGTH), so each token has to stay just under that. Going over gives you a ValueError and a 500, which is a different and much weaker problem.
gunicorn defaults are limit_request_field_size 8190 and limit_request_fields 100, so the working shape is 96 separate Accept fields with one ~8.1KB token each. That puts the single request ceiling near 19s, below the default 30s worker timeout, so one request alone does not get a stock gunicorn worker killed by the arbiter. It does not need to. Each request pins one sync worker for its full duration, so on two workers, two held connections wedge the service.
order_by_precedence collects tokens into a set, so identical tokens collapse and the cost mostly disappears. Each token needs its own parameter name, x0=, x1=, x2= and so on.
Where a fix could go, either of these alone would be enough:
Parse each Accept token once per request and reuse the result in the matching loop and in the 406 re-negotiation.
Put an aggregate budget in get_accept_list(), say 8KB total or 64 tokens, and treat anything past it as not acceptable.
The quadratic behavior itself lives in Django's parser, but DRF is the one calling it six times per token with no…
- Record
GHSA-33wh-fxxf-88vv- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Weakness
- CWE-400, CWE-407
- Updated
- 2026-11-02
OSVUBUNTU-CVE-2026-114138
7.5
Django REST framework is a toolkit for building Web APIs. Prior to 3.18.2, DefaultContentNegotiation.get_accept_list in rest_framework/negotiation.py accepts an unbounded number and aggregate size of Accept tokens, while select_renderer in that file repeatedly invokes _MediaType parsing from rest_framework/utils/mediatypes.py for each attacker-controlled token, every renderer, and forced 406 renegotiation. A remote unauthenticated client can send many distinct media types containing large quoted parameter values with numerous semicolons, causing Django's parameter parser and DRF's repeated parsing structure to consume disproportionate CPU. Requests to any reachable endpoint can pin synchronous workers for prolonged periods, and concurrent requests can make the service unavailable until the requests stop. This issue is fixed in version 3.18.2.
- Record
UBUNTU-CVE-2026-114138- CVSS 3.1
- 7.5 High
CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:N/A:H - Updated
- 2026-11-02
OSVopenSUSE-SU-2026:12277-1 No score
python313-djangorestframework-3.18.2-1.1 on GA media
These are all security issues fixed in the python313-djangorestframework-3.18.2-1.1 package on the GA media of openSUSE Tumbleweed.
- Record
openSUSE-SU-2026:12277-1- Updated
- 2026-11-02
OSVROOT-APP-PYPI-CVE-2026-114138 No score
CVE-2026-114138 in djangorestframework - Patched by Root
Root has patched CVE-2026-114138 in the djangorestframework package for Root:PyPI. Multiple fixed versions available.
- Record
ROOT-APP-PYPI-CVE-2026-114138- Updated
- 2026-11-02
Affected products
Affected packages
| Package | Affected | Fixed |
|---|---|---|
djangorestframeworkPyPI |
before 3.18.2 | 3.18.2 |
djangorestframeworkDebian:12 |
All versions, plus listed versions 3.14.0-23.14.0-2+deb12u13.15.1-13.15.1-23.15.2-1 Show 9 moreShow less3.15.2-23.16.0-13.16.1-13.16.1-33.16.1-43.17.1+ds1-13.17.1-13.18.1+ds1-13.18.3+ds1-1 |
None known |
djangorestframeworkDebian:13 |
All versions, plus listed versions 3.16.0-13.16.1-13.16.1-33.16.1-43.17.1+ds1-13.17.1-13.18.1+ds1-13.18.3+ds1-1 |
None known |
djangorestframeworkDebian:14 |
before 3.18.3+ds1-1, plus listed versions 3.16.0-13.16.1-13.16.1-33.16.1-43.17.1+ds1-13.17.1-13.18.1+ds1-1 |
3.18.3+ds1-1 |
djangorestframeworkPyPI |
before 3.18.2, plus listed versions 0.10.1.10.2.00.2.10.2.2 Show 45 moreShow less0.2.30.2.40.3.00.3.10.3.20.3.30.4.02.0.02.0.12.0.22.1.02.1.12.1.102.1.112.1.122.1.132.1.142.1.152.1.162.1.172.1.22.1.32.1.42.1.52.1.62.1.72.1.82.1.92.2.02.2.12.2.22.2.32.2.42.2.52.2.62.2.72.3.02.3.12.3.102.3.112.3.122.3.132.3.142.3.22.3.3 |
3.18.2 |
djangorestframeworkRoot:PyPI |
before 3.12.4+aikido.2 | 3.12.4+aikido.23.16.1+aikido.13.17.2+aikido.13.15.1+aikido.13.14.0+aikido.2 |
djangorestframeworkUbuntu:16.04:LTS |
All versions, plus listed versions 2.4.3-23.3.1-13.3.2-13.3.2-2 |
None known |
djangorestframeworkUbuntu:18.04:LTS |
All versions, plus listed versions 3.4.0-23.7.7-1 |
None known |
djangorestframeworkUbuntu:20.04:LTS |
All versions, plus listed versions 3.9.0-23.10.2-1 |
None known |
djangorestframeworkUbuntu:22.04:LTS |
All versions, plus listed versions 3.12.1-13.12.4-13.12.4-23.12.4-2ubuntu13.12.4-2.1ubuntu1 |
None known |
Show 4 moreShow less
| Package | Affected | Fixed |
|---|---|---|
djangorestframeworkUbuntu:24.04:LTS |
All versions, plus listed versions 3.14.0-2 |
None known |
djangorestframeworkUbuntu:26.10 |
All versions, plus listed versions 3.16.1-33.16.1-43.17.1+ds1-1 |
None known |
djangorestframeworkUbuntu:26.04:LTS |
All versions, plus listed versions 3.16.1-13.16.1-3 |
None known |
python-djangorestframeworkopenSUSE:Tumbleweed From an advisory covering 3 CVEs |
before 3.18.2-1.1 | 3.18.2-1.1 |
History
| Recorded | Changed | Sources |
|---|---|---|
| affected, facts | ||
| first indexed | CVE List, EUVD, GitHub Advisories, NVD, OSV |
Dates are when Every CVE recorded each change. Upstream dates may differ.
In short
CVE-2026-114138 is a high vulnerability (CVSS 7.5) in Encode Django-rest-framework, published October 21, 2026. No source lists public exploit code. EPSS puts the chance of exploitation in the next 30 days at 0.60%, the 48th percentile. Fixed in djangorestframework 3.18.2.
References
github.com/encode/django-rest-framework/security/advisories/GHSA-33wh-fxxf-88vv
github.com/encode/django-rest-framework/commit/148ab6523922d5a0c110f76615935e5220b63029
github.com/encode/django-rest-framework/releases/tag/3.18.2
nvd.nist.gov/vuln/detail/CVE-2026-114138
github.com/advisories/GHSA-33wh-fxxf-88vv
github.com/CVEProject/cvelistV5/tree/main/cves/2026/114xxx/CVE-2026-114138.json
Left out here: 94 versions and the rest of 2 source descriptions. The JSON record has them.