PT-2026-80240 · Pypi · Django Cms

Published

2026-08-20

·

Updated

2026-08-20

CVSS v3.1

6.5

Medium

VectorAV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N

Impact

The only authorization gate on the duplicate flow is PageAdmin.has add permission, which checks user can add page(user, site) / user can add subpage(...) — i.e. “may this user create a page at all”. Nothing checks the user’s relationship to the page being copied:
  • cms/admin/forms.pyDuplicatePageForm.source = ModelChoiceField(queryset=Page.objects.all(), widget=HiddenInput()) spans every page in the database, on every site.
  • cms/admin/forms.pyAddPageForm. init returns early when the source widget is hidden, so the queryset is never narrowed to the user’s site/subtree.
  • cms/admin/forms.pyAddPageForm.clean() validates only URL uniqueness; source is never validated against the user.
  • cms/admin/pageadmin.pyduplicate() seeds source from the URL only on GET; on POST the value comes entirely from the request body.
  • cms/admin/forms.pyAddPageForm.save()from source() performs source.copy(..., permissions=False) and copies every placeholder and all plugins of source into a new page on the attacker’s site. Because permissions=False drops the source’s view restrictions, the resulting copy is fully readable by the attacker.
This crosses a real privilege boundary: a staff user restricted (via CMS PERMISSION) to their own site or subtree can exfiltrate the content of restricted pages and of pages belonging to other tenants.
Read-back is trivial (verified): the copy is created on the attacker’s site and, because copy(..., permissions=False) strips the source’s view restrictions, the new page is unrestricted. user can view page() then returns True for it (unrestricted + PUBLIC FOR), so the attacker — or even an anonymous visitor — can read the duplicated content directly from the front end. No further permission on the new page is required.

Proof of concept

  1. Log in as a staff user attacker who has add page permission but no view/change permission on a target (secret / other-site) page SECRET ID.
  2. Send (the URL <id> only needs to be a PageContent the attacker can already see — e.g. one of their own pages; the victim id goes in the POST body):
http
POST /admin/cms/pagecontent/<MY OWN PAGECONTENT ID>/duplicate/ HTTP/1.1
Cookie: sessionid=<attacker session>
Content-Type: application/x-www-form-urlencoded

csrfmiddlewaretoken=...&title=x&slug=x&language=en&source=<SECRET ID>
  1. A new, unrestricted page is created under the attacker’s site containing a verbatim copy of the secret page’s plugins, which the attacker can now preview/edit/read.

Patches

Enforce an object-level permission check on source:
python
class DuplicatePageForm(AddPageForm):
  source = forms.ModelChoiceField(
    queryset=Page.objects.all(),
    required=True,
    widget=forms.HiddenInput(),
  )

  def clean source(self):
    source = self.cleaned data.get("source")
    if source and not user can view page(self. user, source):
      raise ValidationError( ("You do not have permission to copy this page."))
    return source
(user can view page is imported from cms.utils.page permissions.)

Workarounds

Until patched, restrict access to the cms.add page permission to fully-trusted staff, or disable the duplicate action for delegated/limited editors.

References

  • cms/admin/pageadmin.pyduplicate(), has add permission(), get urls()
  • cms/admin/forms.pyDuplicatePageForm, AddPageForm. init /clean/save/from source
  • Regression tests: cms/tests/test forms.py::DuplicatePageFormSecurityTestCase

Fix

Missing Authorization

IDOR

Found an issue in the description? Have something to add? Feel free to write us 👾

Weakness Enumeration

Related Identifiers

GHSA-6X92-6VX4-5FWR

Affected Products

Django Cms