Summary
A web application field that accepted user content appeared to be sanitizing script tags, with initial payloads being removed and appearing empty. However, a bypass using an iframe with the srcdoc attribute allowed JavaScript execution in the browser, confirming a stored XSS vulnerability. This case study details the observation, analysis, proof of concept (PoC), impact, and remediation, along with practical test cases and recommendations for penetration testers.
Background and scope
This issue was discovered during an authorized VAPT engagement. The target application accepted user-provided content on their file-upload endpoint, stored it, and later displayed it in a table view on the admin panel. The scope allowed for safe testing of a stored XSS vulnerability in an input field visible to other users. (Note: Because this was performed on a client application on an internal network, no screenshots are available.)
Initial observation
When testing the content field with common XSS payloads (e.g., direct <script>tags or attribute based payload <svg/onload=alert(1)>), the application accepted the submission but the corresponding table cell displayed as blank or change the size of table cell. <svg> tag is being rendered as an element so I see a big blank table cell, but onload didn't execute (so no alert). This suggested the server or renderer attempted to remove or neutralize script-like content or the app stored <svg> but stripped onload.
Common immediate interpretations:
- The app strips
<script>tags on input (blacklist). - The app encodes/escapes certain characters at render time.
- The app stores content but renders it in a context that strips HTML.
At face value, a blank cell can be mistaken for a secure system however, blank rendering is insufficient evidence that all execution contexts are safe.
Testing strategy
The application performs superficial filtering (e.g., removing known bad tags) but does not perform context‑aware output encoding or inspect nested content inside attributes. Therefore, alternative HTML constructs or attributes may carry executable scripts even after superficial filtering.
Strategy:
- Enumerate rendering contexts (HTML body, attribute, event handler, CSS, URL, iframe).
- Attempt payloads that place executable content in edge contexts (attributes, embedded documents).
- Inspect stored data (API responses or database) to determine whether the filter acts at input-storage or output-rendering.
The working bypass: iframe srcdoc
After several iterations, a payload using an iframe with the srcdoc attribute executed in the browser. The srcdoc attribute allows embedding a complete HTML document directly in the attribute value scripts inside srcdoc are treated as part of the iframe document and will execute.
This bypass works when a filter removes top-level <script> tags but does not inspect attribute values for nested HTML or script content.
<iframe srcdoc="<script>alert(1)</script>"></iframe>
Why srcdoc:
- It creates a separate execution context the iframe's document.
- The content inside
srcdocis parsed and executed by the browser even though it appears inside an attribute value. - Naive filters that operate on a token/substring basis (e.g., remove
<script>) often missscripttokens embedded inside attribute values or encoded sequences.
Proof of concept (PoC)
- Submit content containing an
iframewith asrcdocattribute to the vulnerable input. - View the page where stored content is rendered.
- Observe JavaScript execution inside the iframe when the page is loaded by a user.
Testing checklist to confirm a stored XSS:
- Confirm the input persists in storage (API/DB) or is reflected in server responses.
- Verify execution occurs in another user's browser (or in a separate session).
Analysis why the filter failed
- Blacklisting vs allowlisting — The application likely uses a blacklist (remove specific tags or substrings) instead of a context-aware allowlist or output encoding. Blacklists are brittle and easy to bypass with alternative syntaxes.
- Lack of contextual encoding — Encoding must match the rendering context. The application likely failed to perform attribute‑level or iframe‐document encoding checks.
- Insufficient scanning of attribute values — Filters that scan only top‑level HTML tokens will miss nested payloads inside attributes (like
srcdoc). - Possible double-encoding or normalization gaps — If the app decodes/normalizes input inconsistently between storage and render, bypasses become easier.
Impact assessment
Stored XSS allows an attacker to run arbitrary JavaScript in the context of other users or admin visiting the affected page. Common impacts include:
- Session theft (if cookies are not
HttpOnly). - Performing actions on behalf of the user (CSRF-like behavior through script).
- Displaying fake UI elements to harvest credentials or perform social engineering.
- Compromising administrator sessions or performing privileged actions if admins view content.
Testing checklist for pentesters
When you encounter apparent filtering, don't stop at the blank cell. Work through this checklist:
- Try payloads in different contexts: attributes, iframe, SVG, CSS, URLs.
- Inspect storage (API responses, DB) to see what was saved.
- Test different encodings (UTF-8 escapes, hex/Unicode escapes, entity encodings).
- Check how the application normalizes input during storage and render.
- Confirm successful execution in another user session where possible.
- Capture PoC evidence: Burp requests/responses, page source, and screenshots/video.
Conclusion
This case highlights a recurring theme: filters that remove obvious bad tokens are not sufficient. Modern browsers support a variety of HTML constructs and execution contexts; robust defenses rely on context-aware encoding, allowlisting when HTML is needed, and defense in depth through CSP and secure cookie attributes.
For pentesters, the practical takeaway is simple: when a field appears filtered, assume the filter is incomplete and test edge contexts (attributes, iframe content, SVG, etc.).