# Novu has a XSS sanitization bypass

Novu's email preview sanitizer allowlists all attributes and its denylist misses `oncontentvisibilityautostatechange`, letting crafted HTML execute JavaScript in the dashboard origin.

- Payout: Research
- Severity: high
- Program: Novu
- Found by: JorianWoltjer
- Disclosed: 2026-04-14
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-26wg-9xf2-q495
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-26wg-9xf2-q495
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:ghsa sev:high tech:nodejs technique:source-review vuln:xss

**Target:** Email step editor / HTML preview in Novu dashboard
**Root cause:** sanitize-html is configured with `allowedAttributes: false`, so all attributes pass; a post-hoc `DANGEROUS_ATTRIBUTES` denylist is incomplete and omits `oncontentvisibilityautostatechange`.
**Steps:**
1. Create a new workflow and add an Email step
2. In the email body insert `<a oncontentvisibilityautostatechange="alert(window.origin)" style="display:block;content-visibility:auto">`
3. Wait a second; the XSS fires in the preview showing the current origin
**How found:** Read the sanitizer config, noticed `allowedAttributes: false` combined with a manual `DANGEROUS_ATTRIBUTES` denylist, hypothesized the list was incomplete, then ran every payload from the PortSwigger XSS cheat sheet and found `oncontentvisibilityautostatechange=` slipped through.
**Exploitation:** Attacker prepares a workflow email step containing the payload, then sends the victim a deep link to that email step editor (or abuses the Google/GitHub OAuth flow without completing the code callback to log the victim into the attacker's account where the payload is stored), so the victim's browser renders the preview and executes the script in the Novu dashboard origin.
**Escalation:** Turns a self-XSS-looking issue into stored XSS against other dashboard users via shareable deep links and OAuth login-CSRF-style session fixation.
**Insight:** When a sanitizer allowlists all attributes and relies on a denylist of event handlers, diff the denylist against the full set of modern DOM event handler attributes (e.g. newer ones like oncontentvisibilityautostatechange) rather than classic onerror/onload payloads.

## Proof of concept (copied exactly from the original report)

**XSS payload using oncontentvisibilityautostatechange attribute**

```
<a oncontentvisibilityautostatechange="alert(window.origin)" style="display:block;content-visibility:auto">
```

