CVE-2026-87975
Executive Summary
CVE-2026-87975 is an authorization-bypass vulnerability (CWE-639) in the Django web framework affecting versions 6.1 before 6.1.2, 6.0 before 6.0.9, and 5.2 before 5.2.18. The flaw resides in django.forms.models.BaseModelFormSet.save_existing_objects(), which incorrectly trusted the presence of a primary key on a submitted form instance as proof that the instance belonged to the formset's limiting queryset. An authenticated user who is permitted to submit an affected formset can craft management-form data to mark an object outside that queryset for deletion, deleting rows on which they hold no permission. Impact is limited to integrity (unauthorized deletion of specific database rows), with a CVSS 3.1 base score of 4.3 (AV:N/AC:L/PR:L/UI:N/S:U/C:N/I:L/A:N).
This record — and 170,000+ others — are available via the ValtersIT API. One endpoint replaces five: NVD + GitHub CVE + CISA KEV + EPSS + Nuclei + Exploit-DB, unified.
No signup required
Technical Analysis
Django's model formsets provide a mechanism for editing multiple model instances in a single form submission. A formset can be bound to a limiting queryset, which restricts the set of objects a user is authorized to view or modify. During the save phase, BaseModelFormSet.save_existing_objects() reconciles submitted form data against the database: for each submitted form that represents an existing row, it determines the target instance and applies changes (including deletion, when a form is marked for deletion via the management form).
The vulnerable logic used the presence of a primary key on the form's instance as evidence that the instance belonged to the formset's limiting queryset. This assumption is unsafe for models whose primary key is a field accepted by the form — for example:
- A
OneToOneField(or a parent link) used as the primary key of an inline formset's model. - A natural primary key or UUID primary key that is included in the form's fields.
In these configurations, when a submitted object falls outside the limiting queryset, Django constructs a new, unbound instance. Because the primary key is a form-visible field, an attacker can populate that primary key from submitted data. The presence of that populated primary key is then misinterpreted as evidence that the instance is an existing row within the queryset, so the save path proceeds to act on it — including honoring a deletion request expressed through forged management-form data. The result is deletion of a row outside the limiting queryset without any permission on the targeted object.
Two boundary conditions that materially constrain the vulnerability are stated explicitly in the advisory:
- Models using the default
AutoFieldprimary key are not affected, because anAutoFieldis not accepted/submitted as a form field, so an out-of-queryset instance cannot acquire a primary key from submitted data. - Earlier, unsupported Django series (such as 5.1.x, 5.0.x, and 4.2.x) were not evaluated and may also be affected.
The CVSS vector reflects the operational reality: network exploitable (AV:N), low complexity (AC:L), requires low privileges/authenticated access (PR:L), no user interaction (UI:N), no scope change (S:U), no confidentiality impact (C:N), low integrity impact (I:L — deletion of specific targeted rows), and no availability impact (A:N). The vulnerability class is authorization bypass through user-controlled key / IDOR-like trust of attacker-supplied identifiers (CWE-639).
The reporter credited by Django is Seonggwon Yoon. No public PoC, exploit code, or Nuclei template is currently available for this issue.
Exploitation Scenarios
No public PoC is available; the attack chain below is inferred from the vulnerability class and the advisory text. All function/class names used are those appearing in the NVD description.
Reconnaissance / target identification. The attacker identifies a Django application that exposes a model formset or inline formset whose model uses a non-AutoField primary key that is accepted by the form — specifically a
OneToOneFieldor parent link used as the primary key of an inline formset's model, or a natural or UUID primary key included in the form's fields. The formset must be bound to a limiting queryset so that out-of-queryset objects are meaningfully protected.Prerequisite access. The attacker must hold an authenticated account, and that account must be permitted to submit the affected formset. This establishes PR:L. The attacker does not need any permission on the objects they intend to delete, which is the core of the authorization bypass.
Triggering the flaw class. The attacker submits forged management-form data (the standard formset management fields that express, per form, whether the row is an existing object and whether it is marked for deletion), supplying a primary key value corresponding to an object that lies outside the formset's limiting queryset but whose primary key field is form-acceptable. Because the primary key is attacker-supplied and form-visible, the out-of-queryset object is represented by a newly constructed instance carrying that primary key;
save_existing_objects()treats the populated primary key as evidence of queryset membership and processes the deletion.Post-exploitation impact. The targeted database row is deleted even though the attacker had no permission on it. The integrity impact is scoped to the rows an attacker can enumerate and whose primary keys fit the criteria above. There is no confidentiality or availability impact under the CVSS assessment; however, unauthorized deletion of business records (e.g., linked child records, profile records, or referenced objects) can cascade into workflow and data-integrity problems depending on the application's own referential design.
Impact Assessment
The blast radius is confined to Django deployments that (a) use a model formset or inline formset bound to a limiting queryset, and (b) use a non-default primary key that is accepted by the form — i.e., a OneToOneField or parent link as the primary key of an inline formset model, or a natural/UUID primary key present among the form's fields. Applications relying on the default AutoField are explicitly not affected, which substantially narrows the exposed population.
CVSS subscores quantify the impact: Integrity: Low (I:L) captures the ability to delete specific, targeted rows without permission; Confidentiality: None (C:N) and Availability: None (A:N) reflect that no data is read and no service is taken down. The PR:L subscore confirms an authenticated account is required, and UI:N confirms no victim interaction is needed — the attacker acts directly. Therefore, the practical business risk is unauthorized destruction of targeted records by any authenticated user with formset submission rights, which can matter significantly in applications where such records underpin billing, identity, or tenant-scoped data. Because no patch evidence is present in tracked sources and no vendor workaround is documented, operational exposure persists for organizations unable to move to the fixed releases.
Detection Guidance
Sigma Rule
title: Django Formset Out-of-Queryset Deletion Attempt (CVE-2026-87975)
id: 9f1c2b74-3a5e-4d18-8b0c-django87975sig
status: experimental
description: Detects POST requests to Django formset endpoints containing management-form
deletion markers (DELETE) combined with primary-key values, consistent with forged
management-form data used to delete objects outside a formset limiting queryset.
references:
- https://nvd.nist.gov/vuln/detail/CVE-2026-87975
author: valtersit.com
date: 2026/01/01
logsource:
category: webserver
detection:
selection_method:
http.method: 'POST'
selection_delete_marker:
http.request.body|contains:
- 'DELETE'
selection_mgmt_fields:
http.request.body|contains:
- 'INITIAL_FORMS'
- 'TOTAL_FORMS'
- 'MAX_NUM_FORMS'
selection_pk:
http.request.body|contains:
- '-id='
- 'pk='
condition: selection_method and selection_delete_marker and selection_mgmt_fields and selection_pk
falsepositives:
- Legitimate formset submissions that delete rows the user is authorized to delete
level: medium
tags:
- attack.t1190
- cve.2026.87975
YARA Rule
rule CVE_2026_87975_Django_Formset_Deletion
{
meta:
description = "Detects request bodies attempting forged Django formset management-form deletion (CVE-2026-87975)"
author = "valtersit.com"
reference = "https://nvd.nist.gov/vuln/detail/CVE-2026-87975"
date = "2026-01-01"
severity = "medium"
strings:
$mgmt1 = "TOTAL_FORMS"
$mgmt2 = "INITIAL_FORMS"
$mgmt3 = "MAX_NUM_FORMS"
$del = "DELETE"
$pk1 = "-id="
$pk2 = "pk="
condition:
2 of ($mgmt*) and $del and 1 of ($pk*)
}
Suricata IDS Rule
alert http any any -> any any (msg:"CVE-2026-87975 Django formset forged management-form deletion attempt"; flow:established,to_server; http.method; content:"POST"; http.request_body; content:"TOTAL_FORMS"; http.request_body; content:"INITIAL_FORMS"; http.request_body; content:"DELETE"; http.request_body; content:"-id="; classtype:attempted-user; reference:cve,2026-87975; reference:url,nvd.nist.gov/vuln/detail/CVE-2026-87975; sid:202687975; rev:1;)
Elastic Detection
sequence by source.ip with maxspan=5s
[ network where http.request.method == "POST" and
http.request.body contains "TOTAL_FORMS" and
http.request.body contains "INITIAL_FORMS" ]
[ network where http.request.method == "POST" and
http.request.body contains "DELETE" and
(http.request.body contains "-id=" or http.request.body contains "pk=") ]
Splunk SPL Query
index=web sourcetype=access_combined OR sourcetype=django
| where http_method="POST" OR request_method="POST"
| eval body=coalesce(request_body, form_data, "")
| where match(body,"TOTAL_FORMS") AND match(body,"INITIAL_FORMS") AND match(body,"DELETE") AND (match(body,"-id=") OR match(body,"pk="))
| stats count min(_time) as first_seen max(_time) as last_seen values(uri) as uris by src_ip
| where count > 0
| convert ctime(first_seen) ctime(last_seen)
Wazuh OSSEC Rule
<group name="web,django,cve-2026-87975">
<rule id="2026879" level="8">
<decoded_as>web-accesslog</decoded_as>
<field name="method">POST</field>
<regex type="pcre2">TOTAL_FORMS.*INITIAL_FORMS.*DELETE.*(?:-id=|pk=)</regex>
<description>CVE-2026-87975: Possible Django formset forged management-form deletion attempt</description>
<mitre>
<id>T1190</id>
</mitre>
<group>cve_2026_87975,</group>
</rule>
</group>
Mitigation & Patch Guidance
Patch confidence: 0/4 (UNPATCHED — no patch evidence from any tracked source). The upstream advisory names Django 6.1.2, 6.0.9, and 5.2.18 as the releases that resolve the issue, but no patch artifact or commit evidence is currently tracked in the sources feeding this report.
Immediate mitigations. Restrict who may submit affected formsets by tightening the application-layer authorization around any view that processes a model formset or inline formset and allows deletion, especially where the model's primary key is a
OneToOneField, a parent link, or a natural/UUID primary key present in the form's fields. Because the flaw is triggered by forged management-form data marking an out-of-queryset object for deletion, disable formset deletion functionality (can_delete=False) on affected formsets as a stopgap where deletion is not business-required. Review existing records protected only by a limiting queryset for unexpected deletions, and audit logged formset POSTs for out-of-queryset primary keys.Patch/upgrade steps. Upgrade to Django 6.1.2 or later, 6.0.9 or later, or 5.2.18 or later, as applicable to the deployed minor series. Because the issue originates in framework code (
BaseModelFormSet.save_existing_objects()), application-level monkeypatching should be treated only as a temporary measure. Note that earlier unsupported series (5.1.x, 5.0.x, 4.2.x) were not evaluated and may also be affected; schedule migration off end-of-life branches.Long-term hardening. Avoid relying on formset limiting querysets as the sole authorization boundary — enforce object-level permissions in the view or model layer before any deletion is committed. Prefer the default
AutoFieldand avoid exposing primary-key fields to forms where not strictly required. Add regression tests asserting that formset submissions cannot delete objects outside the intended queryset, including forged management-form inputs, and monitor for anomalous bulk-deletion patterns.
References
- NVD — CVE-2026-87975: https://nvd.nist.gov/vuln/detail/CVE-2026-87975
- Django security releases / advisory (fixed versions named as 6.1.2, 6.0.9, 5.2.18): https://www.djangoproject.com/weblog/
- CWE-639 — Authorization Bypass Through User-Controlled Key: https://cwe.mitre.org/data/definitions/639.html
- Django
BaseModelFormSetdocumentation: https://docs.djangoproject.com/en/stable/topics/forms/modelforms/ - CISA KEV Catalog (not currently listed for this CVE): https://www.cisa.gov/known-exploited-vulnerabilities-catalog
No public PoC, exploit code, or Nuclei template is currently associated with CVE-2026-87975, and no threat actor campaign or exploit-in-the-wild evidence is available at the time of writing.
What is this? Security researchers use these exact search queries on Shodan.io (a search engine for internet-connected devices) to find vulnerable systems globally. You can copy these dorks to see if your organization's IP space is exposed.