Your RSA-2048 keys break in 2030. Find every one of them before attackers do.
📦
📦 npm
Not in CISA KEV
CRITICAL severity

CVE-2026-106102 — quasar

CRITICALFix: quasarframework/quasar@11505af

CVE-2026-106102 is a critical-severity (CVSS 10) Cross-site Scripting (XSS) vulnerability in quasar. A fix is available for quasar — see the affected versions and patch details below.

Quasar Framework: Stored/Reflected XSS via unescaped SSR meta tag rendering in getHead()

Also known asGHSA-pq96-jpmf-w254
Published
Updated
Affected
1 pkg
Patched
1 / 1
Exploits
None indexed
Exploitation data as of Oct 22, 2026 · OSV.dev, NVD, FIRST.org (EPSS)

EPSS Exploitation Probability

via FIRST.org ↗
0.3%probability of exploitation in next 30 days
Lower Risk0.00%
Lower risk than most CVEs21th percentile — riskier than 21% of all scored CVEsHighest risk

Probability of exploitation in the next 30 days, from FIRST.org EPSS.

How urgent is this, really

CVE-2026-106102 by exploitation likelihood (EPSS) against impact (CVSS). Outside the shaded patch-first corner.

Where this sits among everything scored

Of 394,803 CVEs with a current EPSS score, this one falls in the < 10% band (highlighted). Counts from FIRST.org, log-scaled.

Real-World Exposure

1 pkg affected
📦quasar

Real-time download stats are indexed for npm and PyPI packages. This vulnerability affects npm packages — download data is not available via public APIs for these ecosystems.

Description

Vulnerability Details

File: ui/src/utils/meta/Meta.js — actually ui/src/plugins/meta/Meta.js (lines 149-176: getAttr() and getHead()) Sink: injectServerMeta() (same file) → ctx.headTags += getHead(data), interpolated verbatim into the raw HTTP response <head> by the production SSR template (app-vite/templates/entry/ssr-prod-webserver.js + app-vite/lib/plugins/vite.html.js) Entry point: the public useMeta() composable (ui/src/composables/use-meta/use-meta.js) — the single documented way apps set page title/meta/link/script tags

Root Cause

getHead() is Quasar's SSR-only serializer that turns the meta/link/script/title data collected from every useMeta() call into a literal HTML string, using plain template-literal interpolation with zero HTML-entity escaping and zero attribute-quote escaping:

function getAttr(seed) {
  return att => {
    const val = seed[att]
    return att + (val !== true && val !== void 0 ? `="${val}"` : '')
  }
}

function getHead(meta) {
  let output = ''
  if (meta.title) {
    output += `<title>${meta.title}</title>`
  }
  ...
}

Contrast this with the client-side equivalent, apply() (same file, used only in the browser post-hydration), which builds the same tags via document.createElement(...) + tag.setAttribute(att, val) — the browser DOM API automatically escapes attribute values, so the client-side path is not vulnerable. getHead() is a separate, parallel implementation for the SSR case that never received the same protection.

Any string containing </title>, ", or > in a title, meta.*.content, link.*.href, or any other attribute value passed to useMeta() breaks out of its intended HTML context and injects arbitrary markup — including a live <script> tag — directly into the raw HTML response sent to every visitor.

Attack Scenario

  1. A Quasar SSR application renders dynamic page metadata via the standard, documented useMeta() pattern — e.g. useMeta(() => ({ title: post.title, meta: { description: { name: 'description', content: post.excerpt } } })) for a blog/CMS/product page.
  2. An attacker who can influence that underlying text (submit a blog post/comment, set their own profile display name, control a field in an integrated CMS/API) sets it to My Post</title><script>alert(document.cookie)</script>.
  3. On every SSR render of that page — for every visitor — getHead() emits this payload unescaped directly into the <head> of the raw HTML response.
  4. The victim's browser parses the HTML top-to-bottom; the injected <script> executes immediately, before hydration, with full access to document.cookie and the DOM.

Impact

  • Type: CWE-79 Cross-Site Scripting
  • Auth required: No (attacker only needs to influence any text that reaches useMeta() — an extremely common pattern, e.g. blog titles, product names, user display names)
  • Consequence: Full client-side script execution in the victim site's origin — session/cookie theft, credential phishing overlays, account takeover, defacement. Unlike a typical XSS bug requiring a specific unusual injection point, this affects the single most common useMeta() use case (rendering any dynamic title/description), so it can be triggered even by ordinary content containing &, <, or " without malicious intent, in addition to being trivially exploitable deliberately.

Vulnerable Code (ui/src/plugins/meta/Meta.js lines 149-176)

function getAttr(seed) {
  return att => {
    const val = seed[att]
    return att + (val !== true && val !== void 0 ? `="${val}"` : '')
  }
}

function getHead(meta) {
  let output = ''
  if (meta.title) {
    output += `<title>${meta.title}</title>`
  }
  ;['meta', 'link', 'script'].forEach(type => {
    const metaType = meta[type]
    for (const att in metaType) {
      const attrs = Object.keys(metaType[att])
        .filter(item => item !== 'innerHTML')
        .map(getAttr(metaType[att]))
      output += `<${type} ${attrs.join(' ')} data-qmeta="${att}">`
      if (type === 'script') {
        output += (metaType[att].innerHTML || '') + '</script>'
      }
    }
  })
  return output
}

Recommended Fix

function escapeHtml(val) {
  return String(val)
    .replaceAll('&', '&amp;')
    .replaceAll('<', '&lt;')
    .replaceAll('>', '&gt;')
    .replaceAll('"', '&quot;')
}

function getAttr(seed) {
  return att => {
    const val = seed[att]
    return att + (val !== true && val !== void 0 ? `="${escapeHtml(val)}"` : '')
  }
}

function getHead(meta) {
  let output = ''
  if (meta.title) {
    output += `<title>${escapeHtml(meta.title)}</title>`
  }
  // ... rest unchanged, script innerHTML intentionally left raw (JSON-LD use case)
}

Verification

Confirmed end-to-end on v2.21.1 via a local Node.js HTTP lab harness that imports the real, unmodified Meta.js source and reproduces the exact useMeta() → injectServerMeta() call sequence a real SSR app performs, reached via actual curl requests (not just isolated function calls):

  1. POST /submit with {"title":"My Post</title><script>alert(document.cookie)</script>","description":"\"><script>alert(document.domain)</script>"}
  2. GET /render/1 returned a raw HTTP response whose <head> contained: <title>My Post</title><script>alert(document.cookie)</script></title><meta name="description" content=""><script>alert(document.domain)</script>" data-qmeta="description">
  3. Verified via grep: 0 occurrences of &lt; (nothing was escaped), 1 occurrence of a literal, unescaped <script>alert(...)</script> tag in the actual HTTP response body.

A fix branch (fix/xss-meta-tag-escaping) is ready with the minimal patch above (adds an escapeHtml() helper used in getAttr()/getHead()); re-running the same PoC against the patched code shows the payload fully HTML-entity-encoded (&lt;script&gt;...) with no live <script> tag, while normal titles containing & still render correctly (&amp;).

Affected Packages

1 total 1 fixed
EcosystemPackageVulnerable rangeFix
📦npmquasarall versions2.22.0npm install quasar@2.22.0

Detection & mitigation playbook

Open-source dependency
  1. Detect

    Scan your dependency tree (package-lock.json, pnpm-lock.yaml, requirements.txt, go.sum, etc.) for quasar, including transitive dependencies — a direct dependency you never call can still pull in a vulnerable version.

  2. Fix

    Update quasar to 2.22.0 or later, then make sure no transitive (indirect) dependency still pins the vulnerable range — O3 confirms CVE-2026-106102 is resolved across your whole dependency graph.

  3. Workarounds

    Escape or sanitise the affected output on the server side rather than relying on client-side filtering, and add a Content-Security-Policy that blocks inline script execution so injected markup cannot run even if it reaches the page.

Frequently Asked Questions

## Vulnerability Details **File**: `ui/src/utils/meta/Meta.js` — actually `ui/src/plugins/meta/Meta.js` (lines 149-176: `getAttr()` and `getHead()`) **Sink**: `injectServerMeta()` (same file) → `ctx.headTags += getHead(data)`, interpolated verbatim into the raw HTTP response `<head>` by the production SSR template (`app-vite/templates/entry/ssr-prod-webserver.js` + `app-vite/lib/plugins/vite.html.js`) **Entry point**: the public `useMeta()` composable (`ui/src/composables/use-meta/use-meta.js`) — the single documented way apps set page title/meta/link/script tags ### Root Cause `getHead()` i
O3 Security · Impact-Aware SCA

Is CVE-2026-106102 in your dependencies?

Find it across npm, including transitive dependencies.

CVE-2026-106102: quasar XSS — Fixed in 2.22.0