# Stored XSS via unescaped persisted fields

Persisted value reaching an HTML, attribute, URL, script, eval or CSS sink without context-correct encoding

_Area: Web injection and code execution. Seen in 78 disclosed reports; 42 confirmed bounties, median $1,232, range $100-$13,950; latest 2026-10-07. Updated 2026-10-23._

## How it works
A value the attacker controls is persisted (a profile field, config property, log entry, uploaded file, plugin JSON, dataset metadata, pasted cell) and later emitted into a page without encoding for that output context. The broken assumption is that stored data is "already safe" or "already trusted" because it passed a filter, a sanitizer, or came from a trusted-looking source. Any decode pass, re-render, alternate sink, or content-type reinterpretation turns the stored bytes back into live markup. The write path can be cross-site: an unauthenticated request that only sets a Referer can trigger a redirect whose query param is logged [E2](https://bugbountylibrary.com/r/h1%3A127948.md).

## Where it shows up
- Fields rendered via `html()`, `dangerouslySetInnerHTML`, `innerHTML`, or template interpolation with no escaping.
- Filters that re-run substitution on already-rendered markup, so expansion lands inside attributes [E1](https://bugbountylibrary.com/r/h1%3A2257080.md).
- Structured config (sort orders, filters) accepting extra attacker properties reaching tooltips/renderers [E8](https://bugbountylibrary.com/r/bc%3Ab76de9bc-1016-4400-9f69-81764545d90e.md).
- Charting-library tooltip callbacks that interpolate series or entity names as raw HTML strings.
- Widget parameters passed to `eval()` or other script-execution sinks [E5](https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md).
- Audit/activity plugins logging URL params from a redirect and rendering them in admin views [E2](https://bugbountylibrary.com/r/h1%3A127948.md).
- Plugin JSON where one field is sanitized and a sibling is not [E3](https://bugbountylibrary.com/r/blog%3Ahethical-io-stealing-the-trello-token-by-abusing-a-cross-iframe-xss-on-the-butler-plugin.md).
- Server-side head/meta serialization: SSR `getHead()` building tags by template-literal interpolation, while the client path uses DOM `setAttribute` [E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-pq96-jpmf-w254.md).
- Rich-text attachment paths that accept `text/html` and render it in an editor surface [E5](https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md).
- Import/paste paths that carry a trust flag (`metadata.trusted`) the importer fails to strip [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-6966-vjj6-99xv.md).
- Uploads served inline based on a MIME deny-list, so an obscure type is parsed as HTML [E4](https://bugbountylibrary.com/r/h1%3A85488.md); uploaded caption/subtitle files rendered unescaped by a media player.

## How to review
Trace each stored value from write to every render sink. The invariant: output encoding happens at the sink, matches the output context, and is not undone by a later decode, trust flag, or content-type guess.
1. Write path: what types/values can be stored? Reject unknown properties and non-string JSON [E8](https://bugbountylibrary.com/r/bc%3Ab76de9bc-1016-4400-9f69-81764545d90e.md). Is the write endpoint CSRF-protected?
2. Sink: is the stored value treated as text, not markup? Check `html()`, `innerHTML`, `dangerouslySetInnerHTML`, template interpolation, `eval()` [E5](https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md).
3. Context: HTML body, attribute, URL (scheme allow-list), `<script>`, and CSS each need their own encoder. In an inline `<script>` block, escape `<`, `>`, and `/` so the value cannot close the block [E4](https://bugbountylibrary.com/r/h1%3A85488.md).
4. Decode pass: does any filter re-parse or re-substitute already-rendered output [E1](https://bugbountylibrary.com/r/h1%3A2257080.md)?
5. Sibling fields: if one field is escaped, are all others in the same stored object [E3](https://bugbountylibrary.com/r/blog%3Ahethical-io-stealing-the-trello-token-by-abusing-a-cross-iframe-xss-on-the-butler-plugin.md)?
6. Parallel paths: does an SSR/head serializer duplicate client rendering with string concatenation instead of DOM APIs [E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-pq96-jpmf-w254.md)?
7. Trust flags: does any import/paste path let the payload declare itself trusted [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-6966-vjj6-99xv.md)?
8. Delivery: is inline rendering gated by an allow-list rather than a deny-list [E4](https://bugbountylibrary.com/r/h1%3A85488.md)?

## Variants seen
- Filter re-running substitution on rendered inner_html, injecting attributes and mXSS via SVG/style [E1](https://bugbountylibrary.com/r/h1%3A2257080.md).
- Log/audit plugin rendering unsanitized URL params; unauthenticated write via a redirect whose param is logged, admin-context read [E2](https://bugbountylibrary.com/r/h1%3A127948.md).
- One plugin field escaped, a sibling field not [E3](https://bugbountylibrary.com/r/blog%3Ahethical-io-stealing-the-trello-token-by-abusing-a-cross-iframe-xss-on-the-butler-plugin.md).
- MIME deny-list miss letting an upload render as HTML (iOS WebKit) [E4](https://bugbountylibrary.com/r/h1%3A85488.md).
- Rich-text attachment accepting `text/html`, unsanitized in the editor surface [E5](https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md).
- SSR meta/head serializer interpolating unescaped values, executing pre-hydration [E6](https://bugbountylibrary.com/r/ghsa%3AGHSA-pq96-jpmf-w254.md).
- Clipboard paste preserving attacker-supplied `metadata.trusted`, bypassing sanitization [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-6966-vjj6-99xv.md).
- Extra config properties reaching a tooltip renderer [E8](https://bugbountylibrary.com/r/bc%3Ab76de9bc-1016-4400-9f69-81764545d90e.md).
- Stored widget text passed to `eval()` on click, shared across users [E5](https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md).

## Impact and severity
Script execution in victims' (often admin) sessions, enabling privilege escalation and further injection [E2](https://bugbountylibrary.com/r/h1%3A127948.md). Severity rises with admin context, same-origin iframes [E3](https://bugbountylibrary.com/r/blog%3Ahethical-io-stealing-the-trello-token-by-abusing-a-cross-iframe-xss-on-the-butler-plugin.md), and CSP-allowed iframe srcdoc [E8](https://bugbountylibrary.com/r/bc%3Ab76de9bc-1016-4400-9f69-81764545d90e.md). Trust-flag bypasses are worse because they defeat the sanitizer entirely [E7](https://bugbountylibrary.com/r/ghsa%3AGHSA-6966-vjj6-99xv.md). `eval()` sinks are worse because CSP with `unsafe-eval` permits them and the payload runs on any viewer [E5](https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md).

## Evidence
- E1: https://bugbountylibrary.com/r/h1%3A2257080.md
- E2: https://bugbountylibrary.com/r/h1%3A127948.md
- E3: https://bugbountylibrary.com/r/blog%3Ahethical-io-stealing-the-trello-token-by-abusing-a-cross-iframe-xss-on-the-butler-plugin.md
- E4: https://bugbountylibrary.com/r/h1%3A85488.md
- E5: https://bugbountylibrary.com/r/ghsa%3AGHSA-gvf2-2rh5-mpgf.md
- E6: https://bugbountylibrary.com/r/ghsa%3AGHSA-pq96-jpmf-w254.md
- E7: https://bugbountylibrary.com/r/ghsa%3AGHSA-6966-vjj6-99xv.md
- E8: https://bugbountylibrary.com/r/bc%3Ab76de9bc-1016-4400-9f69-81764545d90e.md
