# Quasar Framework: SSR/SSG dev error page discloses the full shell environment and its </script> escape is bypassable

Quasar SSR/SSG dev error page dumps process.env, headers and cookies to any network peer, and its case-sensitive </script> escape is bypassable for XSS.

- Payout: Research
- Severity: high
- Program: Quasar
- Found by: fg0x0
- Disclosed: 2026-10-07
- CVE: CVE-2026-106106
- Write-up quality: 8/10
- Original: https://github.com/advisories/GHSA-r5mf-4r5x-q78f
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-r5mf-4r5x-q78f
- Tags: area:web domain:web impact:data-leak kind:research platform:ghsa sev:high tech:nodejs technique:parser-differential technique:source-review vuln:info-disclosure vuln:misconfiguration vuln:xss

**Target:** SSR/SSG dev error page served by @quasar/render-ssr-error via app-vite dev server
**Root cause:** renderSSRError splices JSON.stringify(data) into raw script text and guards only the exact literal `</script>` via replaceAll, while the HTML tokenizer closes script raw text on any case variant of `</script` followed by whitespace, `/` or `>`. Separately, getEnv serializes all of process.env, headers and cookies into the page, and the dev server binds 0.0.0.0.
**Steps:**
1. Start an SSR/SSG Quasar dev server (or the harness calling `renderSSRError({ err, req, rootFolder })` on port 3200 bound to 0.0.0.0).
2. Plain `GET /` with no payload: the 500 page contains every `process.env` value, all request headers and all cookies (canaries AWS_SECRET_ACCESS_KEY and NPM_TOKEN observed).
3. For injection, send a header value like `<TAG><img src=/nonexistent-image onerror='window.__QPWN=document.domain+":"+location.port'>` preceded by `</SCRIPT >`, `</script >`, `</script/>` or `</ScRiPt>`; single quotes because JSON.stringify escapes `"`.
4. Load the page in a browser: the variant closes the `<script type="module">` element and the injected img onerror fires; the exact `</script>` spelling is neutralized (negative control).
5. For developer-browser execution, deliver the payload via a cookie value (URL-decoded before serialization) set from any localhost/127.0.0.1 origin, since it is sent to every port on that host.
**How found:** Noticed the page serializes env/headers/cookies into a script element and that the guard is a literal string replaceAll; reasoned about HTML tokenizer raw-text end rules (case-insensitive `script` plus tab/newline/FF/space/`/`/`>`) versus exact-string matching, then compared to the correct case-insensitive regex in ui/src/plugins/meta/Meta.js and validated with a harness plus headless Chromium positive/negative controls.
**Exploitation:** Unauthenticated network peer polls the dev server until an SSR render throws, then reads cloud keys, registry tokens and DB URLs from the 500 page. XSS requires the payload to arrive in the developer's own request (cookie channel, since cross-origin fetch cannot carry `<`/`>` and req.url is not decoded); no CSP is sent.
**Escalation:** Chains info disclosure with HTML injection: leaked tokens plus script execution in the dev server origin; cookie channel widens injection reach across localhost ports.
**Insight:** When a sink escapes `</script>` with a literal replaceAll, enumerate tokenizer-legal variants (`</SCRIPT>`, `</script >`, `</script/>`) — and check whether the same project already has a correct case-insensitive regex elsewhere to cite as the intended fix. Also check dev-server host overrides: frameworks that force 0.0.0.0 turn any dev-only error page into a network-reachable secret dump.

