# @tinacms/web-components: `tina-markdown` writes rich-text link URLs into `href` without scheme validation, allowing stored XSS

tina-markdown web component assigns rich-text link URLs to anchor href without scheme validation, so a CMS-authored javascript: link executes stored XSS on click in the site origin.

- Payout: Research
- Severity: high
- Program: TinaCMS
- Found by: sondt99, iaohkut-from-NightWolf-Team
- Disclosed: 2026-10-09
- CVE: CVE-2026-108260
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-c42q-qvc3-j6vg
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-c42q-qvc3-j6vg
- Tags: area:web domain:web impact:ato impact:data-leak kind:research platform:ghsa sev:high tech:nodejs technique:chained technique:source-review vuln:xss

**Target:** <tina-markdown> web component rendering rich-text AST
**Root cause:** The anchor branch assigns node.url directly to el.href with no scheme check; the DOMPurify call only handles raw html/html_inline nodes and returns before reaching the anchor branch, so node.url is never sanitized.
**Steps:**
1. Clone tinacms at 0d38acfdd23143384b8787d5d772b713fa7af163; copy packages/@tinacms/web-components/src/tina-markdown.js and packages/@tinacms/mdx/src/sanitize-url.ts into a test dir with dompurify@3.3.1, esbuild, playwright.
2. Build entry.js importing tina-markdown.js (registers the element) and exposing sanitizeUrl as window.__sanitizeUrl.
3. Serve an index.html that creates a <tina-markdown> with content set to an AST containing `{type:'a', url:"javascript:window.__pwned=...", children:[{type:'text',text:'click me'}]}`.
4. Bundle with `npx esbuild entry.js --bundle --outfile=site/bundle.js --format=esm --loader:.ts=ts` and run the Playwright script against a loopback server.
5. Read the shadowRoot anchor's href attribute, click it, and check window.__pwned; compare against sanitizeUrl output for the same input.
**How found:** Noticed tina-markdown.js:75 assigns node.url to el.href with no sanitizer while six sibling renderers (React, Astro, mdx sanitize-url) all call sanitizeUrl; confirmed the DOMPurify call at :68 only covers html nodes and returns before the anchor branch, and that tests only cover https links.
**Exploitation:** A content author stores a rich-text link with a javascript: URL; any visitor clicking the rendered anchor executes attacker script in the site origin. Since TinaCMS admin is served same-origin and the editor token lives in localStorage['tinacms-auth'], a clicking editor/admin leaks that credential to the payload.
**Escalation:** Same-origin admin panel plus localStorage-stored editor token turns content-edit-only access into credential theft from privileged visitors.
**Insight:** When a repo has many renderers of the same AST, diff the sinks: the one component that skips the shared sanitizeUrl helper is the bug. Also check whether the sanitizer call sits on a branch that returns before the vulnerable assignment.

