Skip to content

fix(tldraw): allow data:image/svg+xml on <image> with recursive sanitization#8087

Merged
MitjaBezensek merged 3 commits intomainfrom
mitja/nested-svg
Feb 25, 2026
Merged

fix(tldraw): allow data:image/svg+xml on <image> with recursive sanitization#8087
MitjaBezensek merged 3 commits intomainfrom
mitja/nested-svg

Conversation

@MitjaBezensek
Copy link
Copy Markdown
Contributor

@MitjaBezensek MitjaBezensek commented Feb 25, 2026

When you export shapes as SVG, paste back (creates image shape), then export again with other shapes — the second export wraps the first SVG as <image href="data:image/svg+xml;base64,...">. On paste, sanitizeSvg() strips this href because sanitizeUri only allows raster data URIs on <image> elements. Result: blank image.

This PR decodes the embedded SVG, runs sanitizeSvg() recursively (strips scripts/XSS), then re-encodes. Fully malicious embedded SVGs (no safe content left) are still blocked. Also adds a MAX_NODE_DEPTH=100 cap on sanitizeNode recursion to prevent DoS from deeply nested elements.

Change type

  • bugfix

Test plan

  1. Export a geo shape as SVG
  2. Paste it back (creates image shape)
  3. Select that image shape + another shape, export as SVG
  4. Paste the resulting SVG — the image should render correctly, not be blank
  • Unit tests

Release notes

  • Fixed SVG sanitizer stripping embedded SVG data URIs on <image> elements. Nested SVGs (from re-exporting pasted SVG images) are now recursively sanitized instead of blocked.

Note

Medium Risk
Touches XSS-sensitive SVG sanitization logic and introduces recursive decoding/encoding of data URIs, so subtle parsing/encoding or missed edge cases could create security or rendering regressions.

Overview
Fixes a regression where exported SVGs containing nested <image href="data:image/svg+xml;base64,..."> were being blanked by the sanitizer.

sanitizeSvg now allows data:image/svg+xml on <image>/<feImage> by decoding the embedded SVG, re-running sanitization recursively, and re-encoding the sanitized result (dropping the href entirely if the embedded SVG has no safe content). Adds an embed recursion depth limit and new unit tests covering safe embedded SVG preservation and script-stripping behavior.

Written by Cursor Bugbot for commit 8297037. This will update automatically on new commits. Configure here.

@huppy-bot huppy-bot bot added the bugfix Bug fix label Feb 25, 2026
@vercel
Copy link
Copy Markdown

vercel bot commented Feb 25, 2026

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

Project Deployment Actions Updated (UTC)
examples Ready Ready Preview Feb 25, 2026 1:58pm
5 Skipped Deployments
Project Deployment Actions Updated (UTC)
analytics Ignored Ignored Preview Feb 25, 2026 1:58pm
chat-template Ignored Ignored Preview Feb 25, 2026 1:58pm
tldraw-docs Ignored Ignored Preview Feb 25, 2026 1:58pm
tldraw-shader Ignored Ignored Preview Feb 25, 2026 1:58pm
workflow-template Ignored Ignored Preview Feb 25, 2026 1:58pm

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 1 potential issue.

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

@MitjaBezensek MitjaBezensek added the dotcom-hotfix-please ⚙️ Triggers tldraw.com production deploy after merge label Feb 25, 2026
@MitjaBezensek MitjaBezensek added this pull request to the merge queue Feb 25, 2026
@MitjaBezensek MitjaBezensek removed this pull request from the merge queue due to a manual request Feb 25, 2026
@MitjaBezensek MitjaBezensek added this pull request to the merge queue Feb 25, 2026
Merged via the queue into main with commit b245c8e Feb 25, 2026
18 checks passed
@MitjaBezensek MitjaBezensek deleted the mitja/nested-svg branch February 25, 2026 14:07
huppy-bot bot pushed a commit that referenced this pull request Feb 25, 2026
…ization (#8087)

When you export shapes as SVG, paste back (creates image shape), then
export again with other shapes — the second export wraps the first SVG
as `<image href="data:image/svg+xml;base64,...">`. On paste,
`sanitizeSvg()` strips this href because `sanitizeUri` only allows
raster data URIs on `<image>` elements. Result: blank image.

This PR decodes the embedded SVG, runs `sanitizeSvg()` recursively
(strips scripts/XSS), then re-encodes. Fully malicious embedded SVGs (no
safe content left) are still blocked. Also adds a `MAX_NODE_DEPTH=100`
cap on `sanitizeNode` recursion to prevent DoS from deeply nested
elements.

### Change type

- [x] `bugfix`

### Test plan

1. Export a geo shape as SVG
2. Paste it back (creates image shape)
3. Select that image shape + another shape, export as SVG
4. Paste the resulting SVG — the image should render correctly, not be
blank

- [x] Unit tests

### Release notes

- Fixed SVG sanitizer stripping embedded SVG data URIs on `<image>`
elements. Nested SVGs (from re-exporting pasted SVG images) are now
recursively sanitized instead of blocked.

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches XSS-sensitive SVG sanitization logic and introduces recursive
decoding/encoding of data URIs, so subtle parsing/encoding or missed
edge cases could create security or rendering regressions.
> 
> **Overview**
> Fixes a regression where exported SVGs containing nested `<image
href="data:image/svg+xml;base64,...">` were being blanked by the
sanitizer.
> 
> `sanitizeSvg` now allows `data:image/svg+xml` on `<image>`/`<feImage>`
by decoding the embedded SVG, re-running sanitization recursively, and
re-encoding the sanitized result (dropping the `href` entirely if the
embedded SVG has no safe content). Adds an embed recursion depth limit
and new unit tests covering safe embedded SVG preservation and
script-stripping behavior.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
8297037. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->
MitjaBezensek added a commit that referenced this pull request Feb 25, 2026
…ive sanitization (#8088)

This is an automated hotfix PR for dotcom deployment.

**Original PR:** [#8087](#8087)
**Original Title:** fix(tldraw): allow data:image/svg+xml on <image>
with recursive sanitization
**Original Author:** @MitjaBezensek

This PR cherry-picks the changes from the original PR to the hotfixes
branch for immediate dotcom deployment.

/cc @MitjaBezensek

<!-- CURSOR_SUMMARY -->
---

> [!NOTE]
> **Medium Risk**
> Touches XSS-hardening logic and changes which `data:` URIs are
permitted on `<image>`, so mistakes could reintroduce injection vectors
or break image rendering; recursion depth limiting reduces worst-case
risk.
> 
> **Overview**
> `sanitizeSvg` now allows `data:image/svg+xml` URLs on
`<image>`/`<feImage>` by **decoding and recursively sanitizing** the
embedded SVG, then re-encoding it as a data URI; unsafe/invalid embedded
SVG (or overly deep nesting) causes the `href` to be removed.
> 
> This introduces helpers for data-URI decode/encode, threads a
recursion `depth` through node/attribute sanitization, and expands tests
to cover preserving safe embedded SVG, stripping scripts from
mixed-content embeds, and continuing to block fully malicious embeds.
> 
> <sup>Written by [Cursor
Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit
aa36002. This will update automatically
on new commits. Configure
[here](https://cursor.com/dashboard?tab=bugbot).</sup>
<!-- /CURSOR_SUMMARY -->

Co-authored-by: Mitja Bezenšek <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bugfix Bug fix dotcom-hotfix-please ⚙️ Triggers tldraw.com production deploy after merge

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant