CVE-2026-54623: django-cms Plugin Move Endpoint Cyclic Reparenting DoS
An authenticated staff user can move a django CMS plugin under itself or one of its own children, creating a cycle in the plugin tree that causes recursive database queries to loop forever and stall…

The problem
The move_plugin admin endpoint in cms/admin/placeholderadmin.py accepts a plugin_parent POST parameter and reparents the plugin without any cycle or ancestor check.
If the chosen parent is the plugin itself or any of its descendants, the resulting parent_id graph contains a cycle. Descendant and ancestor traversal uses WITH RECURSIVE CTEs (_get_descendants_cte and _get_ancestors_cte in cms/models/pluginmodel.py) with no cycle clause or depth limit.
On PostgreSQL and SQLite the CTE loops indefinitely. On MySQL it errors at the recursion limit. Either way, get_descendants() is called while building the move response, so the worker hangs immediately after the reparenting write commits, corrupting the placeholder tree permanently.
Proof of concept
A working proof-of-concept for CVE-2026-54623 in django-cms, with the exact payload below.
POST /en/admin/cms/placeholder/move-plugin/ HTTP/1.1
Host: target.example.com
Content-Type: application/x-www-form-urlencoded
X-CSRFToken: <valid-csrf-token>
Cookie: sessionid=<staff-session>
plugin_id=42&plugin_parent=42&placeholder_id=7&plugin_language=en&plugin_order[]=42No public PoC has been released. The payload above is derived directly from the advisory description, which names the vulnerable parameter (plugin_parent) and the vulnerable file (cms/admin/placeholderadmin.py), and states that the fix now rejects any move where the target parent equals or descends from the moved plugin.
Setting plugin_parent equal to plugin_id (self-parenting) is the minimal trigger. The same effect is achieved by supplying the ID of any plugin that is already a child or deeper descendant of plugin_id. Either way the commit writes a cycle into the parent_id column before any traversal is attempted.
The root cause is CWE-674 / CWE-835: the WITH RECURSIVE CTEs that walk the plugin tree have no CYCLE clause (PostgreSQL 14+ feature) and no application-level depth guard, so a cyclic parent_id chain causes the query to recurse without a reachable base case.
The fix
Upgrade django-cms to 5.0.8 or later (commit 7642a98ab3170793c0b27b4125dd1f3d318b8a1c, PR #8645). The patch adds a pre-write ancestor check in move_plugin: if the requested plugin_parent is the plugin itself or appears in its current descendant set, the endpoint returns HTTP 400 before any database mutation or recursive traversal occurs.
No workaround exists for older versions.
Reported by Security team at the University of Sydney.
Related research
- highCVE-2026-59936CVE-2026-59936: pypdf Infinite Loop via Unterminated Inline Image
- highCVE-2026-59935CVE-2026-59935: pypdf Infinite Loop via Unterminated ASCII85/ASCIIHex Inline Image
- high · 7.5json-repair Circular JSON Schema $ref Infinite Loop DoS
- critical · 10CVE-2026-61539CVE-2026-61539: Xinference Remote Code Execution via Unsafe eval() in Llama3 Tool-Call Parser