{"uuid": "51f87591-c95a-456a-bab5-bfea5149efad", "vulnerability_lookup_origin": "1a89b78e-f703-45f3-bb86-59eb712668bd", "author": "9f56dd64-161d-43a6-b9c3-555944290a09", "vulnerability": "CVE-2026-88420", "type": "seen", "source": "https://gist.github.com/itsmohitnarayan/89de718efb225e840719478ed949bd51", "content": "# CVE-2026-88420: Stored XSS via the Markdown field in APSL Puput\n\n## CVE ID\nCVE-2026-88420\n\n## Affected Product\n- Project: APSL/Puput\n- Repository: https://github.com/APSL/puput\n- Package: https://pypi.org/project/puput/\n- Affected components: `puput/abstracts.py` (`EntryAbstract.save`) and `puput/models.py` (`EntryPage.get_preview_context`)\n\n## Affected Versions\nPuput 1.2.1 through 2.2.0. Markdown support was introduced in commit `e904d06`. The vulnerable code remains present in the current `master` branch at commit `ce7a2f5` and in the latest release, 2.2.0.\n\n## Patched Versions\nNone as of 2026-09-22.\n\n## Vulnerability / Problem Type\nCWE-79: Improper Neutralization of Input During Web Page Generation (Stored Cross-site Scripting)\n\n## Summary\nPuput renders the author-controlled `markdown_body` field with Python-Markdown's `markdown.markdown()` function and stores the result in the rich-text `body` field. Python-Markdown passes raw HTML through without sanitising it. Puput then renders `body` to visitors with Wagtail's `richtext` filter.\n\nPuput already depends on `wagtail-markdown`, whose `render_markdown()` function applies `bleach.clean()` with an allowlist that excludes `script`, but Puput bypasses that renderer. An authenticated user with Wagtail Editor privileges can therefore persist arbitrary JavaScript that executes for public visitors and in the Wagtail admin preview.\n\n## Impact\nA user with permission to create or edit a blog entry can execute JavaScript in the origin of the Puput site when another user views the published entry or an administrator opens its preview. This can permit authenticated actions in a victim's session, content modification, data access available to the victim, and privilege escalation within the Wagtail administration interface.\n\n## Proof of Concept\n1. Log in to the Wagtail administration interface as a non-superuser user in the Editors group.\n2. Create or edit a Puput entry.\n3. Enter the following value in **body (Markdown)**:\n\n```html\nalert(document.domain)\n\n# Normal heading\n```\n\n4. Publish the entry.\n5. Open the published entry in a logged-out browser. The JavaScript executes.\n6. Open the entry preview in the Wagtail administration interface. The same payload executes there.\n\nObserved on Puput 2.2.0: `EntryPage.body` contains the `` element verbatim. Blanking `body` and saving the entry regenerates the payload from `markdown_body`, confirming that the vulnerable save path persists it. The same payload entered into Wagtail's standard rich-text field is sanitised.\n\nThe issue was reproduced with Django 4.2.30 and Wagtail 6.2.4, then re-verified with Wagtail 6.4.2.\n\n## Root Cause\n`EntryAbstract.save()` and `EntryPage.get_preview_context()` call `markdown.markdown()` directly instead of the sanitising `wagtailmarkdown.utils.render_markdown()` function. The resulting HTML is trusted and rendered with `|richtext`, which rewrites Wagtail rich-text constructs but does not sanitise arbitrary HTML.\n\n## Remediation\nRender `markdown_body` through `wagtailmarkdown.utils.render_markdown()` in both affected call sites. Re-render and audit every existing entry with a non-empty `markdown_body`, because changing the renderer does not remove payloads already persisted in `body`.\n\n## References\n- Vulnerable save path: https://github.com/APSL/puput/blob/ce7a2f5293c4778f144e16ba9cd5e8ce1c348f27/puput/abstracts.py#L158-L161\n- Vulnerable preview path: https://github.com/APSL/puput/blob/ce7a2f5293c4778f144e16ba9cd5e8ce1c348f27/puput/models.py#L181-L187\n- Public rendering sink: https://github.com/APSL/puput/blob/ce7a2f5293c4778f144e16ba9cd5e8ce1c348f27/puput/templates/puput/entry_page.html#L58\n- Puput 2.2.0 release: https://github.com/APSL/puput/releases/tag/2.2.0\n\n## Disclosure Timeline\n- 2026-08-26: Vulnerability identified and reproduced against Puput 2.2.0.\n- 2026-08-27: CVE ID requested from MITRE.\n- 2026-09-22: MITRE assigned CVE-2026-88420.\n\n## Reporter\nKumar Mohit ([@itsmohitnarayan](https://github.com/itsmohitnarayan)).\n", "creation_timestamp": "2026-09-22T12:43:45.161279Z"}