PT-2026-80240 · Pypi · Django Cms
Published
2026-08-20
·
Updated
2026-08-20
CVSS v3.1
6.5
Medium
| Vector | AV: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.py—DuplicatePageForm.source = ModelChoiceField(queryset=Page.objects.all(), widget=HiddenInput())spans every page in the database, on every site.cms/admin/forms.py—AddPageForm. initreturns early when thesourcewidget is hidden, so the queryset is never narrowed to the user’s site/subtree.cms/admin/forms.py—AddPageForm.clean()validates only URL uniqueness;sourceis never validated against the user.cms/admin/pageadmin.py—duplicate()seedssourcefrom the URL only on GET; on POST the value comes entirely from the request body.cms/admin/forms.py—AddPageForm.save()→from source()performssource.copy(..., permissions=False)and copies every placeholder and all plugins ofsourceinto a new page on the attacker’s site. Becausepermissions=Falsedrops 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
- Log in as a staff user
attackerwho has add page permission but no view/change permission on a target (secret / other-site) pageSECRET ID. - Send (the URL
<id>only needs to be aPageContentthe 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>- 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.py—duplicate(),has add permission(),get urls()cms/admin/forms.py—DuplicatePageForm,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 👾
Related Identifiers
Affected Products
Django Cms