Skip to content

revert(tldraw): revert SVG sanitization#7886

Merged
MitjaBezensek merged 2 commits intomainfrom
mitja/revert-svg-sanitize
Feb 11, 2026
Merged

revert(tldraw): revert SVG sanitization#7886
MitjaBezensek merged 2 commits intomainfrom
mitja/revert-svg-sanitize

Conversation

@MitjaBezensek
Copy link
Copy Markdown
Contributor

@MitjaBezensek MitjaBezensek commented Feb 11, 2026

Reverts #7880 (SVG sanitization). Our SVG export relies on <foreignObject> for text rendering (used by RichTextSVG in text, geo, note, arrow shapes + the fallback HTML renderer in getSvgJsx). The sanitizer correctly strips foreignObject (DOMPurify does the same — it's in their svgDisallowed list), but this breaks copy/paste of our own SVGs since all text content is lost.

Keeps the docs recommendation to host assets on a separate domain — that's the primary defense against same-origin SVG attacks.

Closes #7876

Change type

  • improvement

Test plan

  1. Copy as SVG, paste into tldraw — text should render correctly with fonts
  2. Drop SVG files onto canvas — should work as before
  • Unit tests
  • End to end tests

Release notes

  • Revert SVG sanitization that broke copy/paste of SVGs containing text

@vercel
Copy link
Copy Markdown

vercel bot commented Feb 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Feb 11, 2026 2:35pm
tldraw-docs Ready Ready Preview Feb 11, 2026 2:35pm
4 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Feb 11, 2026 2:35pm
chat-template Ignored Ignored Preview Feb 11, 2026 2:35pm
tldraw-shader Ignored Ignored Preview Feb 11, 2026 2:35pm
workflow-template Ignored Ignored Preview Feb 11, 2026 2:35pm

Request Review

Copy link
Copy Markdown

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 3 potential issues.

Bugbot Autofix is OFF. To automatically fix reported issues with Cloud Agents, enable Autofix in the Cursor dashboard.

const assetInfo = await getAssetInfo(sanitizedFile, options, assetId)
const result = await editor.uploadAsset(assetInfo, sanitizedFile)
const assetInfo = await getAssetInfo(file, options, assetId)
const result = await editor.uploadAsset(assetInfo, file)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SVG sanitization removed across import paths

High Severity

defaultHandleExternalFileAsset, defaultHandleExternalFileContent, and defaultHandleExternalSvgTextContent now pass raw SVG content without sanitizeSvg filtering. This allows scriptable or hostile SVG payloads to be stored and served unchanged, reintroducing same-origin SVG attack exposure for deployments that upload assets to their main domain.

Additional Locations (2)

Fix in Cursor Fix in Web

const assetInfo = await getAssetInfo(sanitizedFile, options)
if (acceptedImageMimeTypes.includes(sanitizedFile.type)) {
editor.createTemporaryAssetPreview(assetInfo.id, sanitizedFile)
const assetInfo = await getAssetInfo(file, options)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Malformed SVG now aborts asset handling

Medium Severity

Removing maybeSanitizeSvgFile means malformed image/svg+xml files are no longer filtered out before getAssetInfo. When MediaHelpers.getImageSize rejects on invalid SVG data, defaultHandleExternalFileContent now throws during the loop, which can fail the whole drop/paste batch instead of skipping the bad file.

Additional Locations (1)

Fix in Cursor Fix in Web

if (!text.includes('<svg')) {
throw new Error('SVG was fully sanitized — it contained no safe content')
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unsanitized SVG inserted into live DOM

High Severity

defaultHandleExternalSvgTextContent now parses untrusted text and appends the resulting <svg> node to document.body for measurement without prior sanitization. This creates a direct DOM-insertion path for attacker-controlled SVG markup during paste handling.

Fix in Cursor Fix in Web

@cloudflare-workers-and-pages
Copy link
Copy Markdown

Deploying with  Cloudflare Workers  Cloudflare Workers

The latest updates on your project. Learn more about integrating Git with Workers.

Status Name Latest Commit Preview URL Updated (UTC)
✅ Deployment successful!
View logs
image-pipeline-template 11c3b69 Commit Preview URL

Branch Preview URL
Feb 11 2026, 02:29 PM

@MitjaBezensek MitjaBezensek changed the title Revert SVG sanitization revert(tldraw): revert SVG sanitization Feb 11, 2026
@MitjaBezensek MitjaBezensek added the sdk Affects the tldraw sdk label Feb 11, 2026
@huppy-bot huppy-bot bot added the improvement Product improvement label Feb 11, 2026
@MitjaBezensek MitjaBezensek added this pull request to the merge queue Feb 11, 2026
Merged via the queue into main with commit 94d6bdd Feb 11, 2026
30 checks passed
@MitjaBezensek MitjaBezensek deleted the mitja/revert-svg-sanitize branch February 11, 2026 15:08
@steveruizok
Copy link
Copy Markdown
Collaborator

Thanks for catching this before we landed it.

github-merge-queue bot pushed a commit that referenced this pull request Feb 23, 2026
Closes #7876

After reverting DOMPurify-based sanitization (#7886) because it stripped
`<foreignObject>` (needed for text) and `<style>` (needed for fonts),
the SDK had no sanitization of pasted/dropped SVGs. This PR adds a
custom allowlist-based sanitizer (~643 lines, <3KB gzip) that blocks
attack vectors while preserving tldraw's own SVG output. The
tag/attribute allowlists and URI sanitization approach are derived from
[DOMPurify](https://github.com/cure53/DOMPurify) (MIT license).

### What it does

**Root element validation:** Rejects documents whose root is not
`<svg>`.

**Mode-switching sanitizer:** Walks the SVG DOM tree, switching between
SVG and HTML sanitization modes at `<foreignObject>` boundaries. This
preserves tldraw's text rendering (foreignObject with HTML) while
applying the correct allowlist in each context.

**Context-dependent URI sanitization:**
- `<image>` / `<feImage>`: raster `data:` only (no `svg+xml` — could
embed unsanitized SVG)
- `<use>`: fragment refs (`#id`) only
- `<a>`: `http:`, `https:`, `mailto:` only

**CSS sanitization:** Strips `@import`, `expression()`, `-moz-binding`,
`behavior:`, and external `url()` references. Preserves `data:`
font/image URLs and `url(#id)` fragment references (needed for
gradients/filters). Handles multiline `url()` values (dotAll flag) and
CSS escape decoding with bounds checks.

**URL-bearing presentation attributes:** Sanitizes `url()` values in
`fill`, `filter`, `clip-path`, `mask`, `stroke`, `marker-*`, `cursor` —
not just `href`/`xlink:href`. Case-insensitive matching. External
`url()` references in these attributes are stripped while `url(#id)`
internal refs are preserved.

**Animation safety:** `<animate>`, `<set>`, `<animateColor>`,
`<animateTransform>` elements targeting `href`, `xlink:href`, or `on*`
via `attributeName` are removed (prevents runtime injection of
`javascript:` URIs that bypass static sanitization).

**Event handler stripping:** All `on*` attributes stripped after
invisible whitespace normalization — catches current and future handlers
without maintaining a blocklist.

**Integration:** Applied at all 4 SVG entry points via dynamic import
(keeps sanitizer out of main bundle). `maybeSanitizeSvgFile` has
try-catch so one bad file doesn't abort a batch. Failed SVG sanitization
shows a toast in multi-file drops. SVG text paste returns silently on
failure. File-replace returns silently on failure (called without
await).

### Change type

- [x] `improvement`

### Test plan

1. Paste tldraw-generated SVG (copy as SVG) — text, fonts, foreignObject
preserved
2. Paste malicious SVGs (script, onerror, onload, javascript: href) —
all blocked
3. Paste SVG with animation targeting href — animation removed
4. Paste SVG with external url() in fill/filter/mask attributes —
stripped
5. Drop multiple files including a malicious SVG — toast shown, other
files still processed

- [x] Unit tests (76 tests covering attack vectors, preservation, and
round-trip with all shape types)

### API changes

- Added `sanitizeSvg(svgText: string): string` — public export for SDK
users building custom handlers

### Release notes

- Add SVG sanitization on paste and file drop to prevent XSS and data
exfiltration
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

improvement Product improvement sdk Affects the tldraw sdk

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Sanitize SVG content in the SDK to prevent XSS

2 participants