revert(tldraw): revert SVG sanitization#7886
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
4 Skipped Deployments
|
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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)
| const assetInfo = await getAssetInfo(sanitizedFile, options) | ||
| if (acceptedImageMimeTypes.includes(sanitizedFile.type)) { | ||
| editor.createTemporaryAssetPreview(assetInfo.id, sanitizedFile) | ||
| const assetInfo = await getAssetInfo(file, options) |
There was a problem hiding this comment.
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)
| if (!text.includes('<svg')) { | ||
| throw new Error('SVG was fully sanitized — it contained no safe content') | ||
| } | ||
|
|
There was a problem hiding this comment.
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.
Deploying with
|
| 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 |
|
Thanks for catching this before we landed it. |
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


Reverts #7880 (SVG sanitization). Our SVG export relies on
<foreignObject>for text rendering (used byRichTextSVGin text, geo, note, arrow shapes + the fallback HTML renderer ingetSvgJsx). The sanitizer correctly stripsforeignObject(DOMPurify does the same — it's in theirsvgDisallowedlist), 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
improvementTest plan
Release notes