feat(tldraw): add custom SVG sanitizer for external content#7896
feat(tldraw): add custom SVG sanitizer for external content#7896MitjaBezensek merged 8 commits intomainfrom
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
5 Skipped Deployments
|
e11e5a2 to
ac86f31
Compare
|
|
||
| async function maybeSanitizeSvgFile(file: File): Promise<File | null> { | ||
| if (file.type !== 'image/svg+xml') return file | ||
| try { |
There was a problem hiding this comment.
Strict MIME check bypasses SVG sanitizer
Medium Severity
maybeSanitizeSvgFile only sanitizes when file.type is exactly image/svg+xml. SVG files with valid MIME variants (for example with parameters) bypass sanitizeSvg and are processed as trusted content when those MIME types are allowed in acceptedImageMimeTypes, creating a sanitizer bypass path.
There was a problem hiding this comment.
DEFAULT_SUPPORTED_VECTOR_IMAGE_TYPES is exactly ['image/svg+xml']. notifyIfFileNotAllowed rejects files before they reach maybeSanitizeSvgFile if their type isn't in the accepted list.
5dab162 to
61f2a31
Compare
| ) { | ||
| const { sanitizeSvg } = await import('./utils/svg/sanitizeSvg') | ||
| text = sanitizeSvg(text) | ||
| if (!text) return |
There was a problem hiding this comment.
SVG text paste can still throw
Low Severity
defaultHandleExternalSvgTextContent dynamically imports sanitizeSvg without a try/catch. If that chunk load fails, paste handling rejects before reaching the new silent-failure path, so SVG text paste can still produce an unhandled error instead of being safely ignored.
There was a problem hiding this comment.
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.
| return match | ||
| } | ||
| return '' | ||
| }) |
There was a problem hiding this comment.
CSS comments bypass URL sanitization
High Severity
sanitizeCssValue matches url( and @import with regexes that do not handle CSS comments. Payloads like url/**/(...) or @import/**/url(...) survive sanitization because CSS treats comments as whitespace. This leaves external resource loads in sanitized SVG/CSS and reintroduces network exfiltration vectors.
Additional Locations (1)
mimecuvalo
left a comment
There was a problem hiding this comment.
this PR does make me pretty nervous to maintain this 😬
couple things to consider before moving forward:
- how do we stay up to date with DOMPurify?
- can this block the main thread if it's a big SVG? should we consider a web worker?
- is this possibly a delay-loaded JS module since not many regular folks would prbly paste in SVGs?
That's why I initially went with using DOMPurify directly, but it did pull 9k minified (or something like that). And I think we decided that's too much. We use We already use a dynamic import const { sanitizeSvg } = await import('./utils/svg/sanitizeSvg') |
mimecuvalo
left a comment
There was a problem hiding this comment.
cool, i didn't see that it was delay-loaded, nice.
LGTM 👍
Add 12 new entries from PRs merged since v4.4.0: - Featured: click-through on transparent image pixels (#7942) - API: enum-to-const-object refactor (#8084) - Improvements: SVG sanitizer (#7896), save-on-blur (#8037) - Bug fixes: cross-origin download (#8090), zero-size draw (#8067), rich text toolbar cleanup (#8050), zoom threshold (#8040), selection foreground fallback (#8011), sticky note SVG shadow (#7934), arrow frame clamping (#7932), zero pressure draw (#5693)
Update `next.mdx` release notes to cover all SDK-relevant PRs merged to main since v4.4.0. Highlights: - Display values system (#8121) with breaking changes and migration guide - Click-through on transparent image pixels (#7942) - `Editor.resizeToBounds()` (#8120) - SVG sanitization (#7896) - TypeScript enum-to-const refactoring (#8084) - 14 bug fixes and 4 improvements ### Change type - [x] `other` ### Test plan 1. Verify the release notes render correctly on the docs site ### Release notes - Update next release notes with changes since v4.4.0. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Low Risk** > Low risk: documentation-only changes updating release notes content and date, with no runtime/code behavior impact. > > **Overview** > Updates `apps/docs/content/releases/next.mdx` for the upcoming release by refreshing the date and replacing the brief blurb with expanded release notes. > > Documents new SDK surface area (`Geometry2d.ignoreHit`, `Editor.resizeToBounds`, `sanitizeSvg`), highlights click-through on transparent image pixels, and adds a list of recent improvements and bug fixes (paste parenting, link/alt-text persistence, SVG sanitization behavior, circular-dependency cleanup, and several crash/export/sync fixes). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 5f7dc0a. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY -->
In order to publish the v4.5.0 release notes and record the v4.4.1 patch, this PR archives release notes and resets `next.mdx` for the next cycle. **v4.5.0.mdx** (new file): - Archived from `next.mdx` with full frontmatter, keywords, and GitHub release link - Featured sections: click-through on transparent image pixels (#7942), breaking `EmbedShapeUtil.configure()` change (#8034) - API changes: `Editor.getResizeScaleFactor()` (#8042), `TLImageAsset.pixelRatio` (#8163), `sanitizeSvg` (#7896), `experimental__onDropOnCanvas` (#7911), enum-to-const refactoring (#8084) - 6 improvements and 20 bug fixes from production **v4.4.0.mdx:** - Add v4.4.1 patch release section with tooltip positioning fix (#8171) - Add v4.4.1 to keywords **next.mdx:** - Reset with `last_version: v4.5.0` and empty content ### Change type - [x] `other` ### Code changes | Section | LOC change | | ------------- | ------------- | | Documentation | +128 / -107 |
In order to publish the v4.5.0 release notes and record the v4.4.1 patch, this PR archives release notes and resets `next.mdx` for the next cycle. **v4.5.0.mdx** (new file): - Archived from `next.mdx` with full frontmatter, keywords, and GitHub release link - Featured sections: click-through on transparent image pixels (#7942), breaking `EmbedShapeUtil.configure()` change (#8034) - API changes: `Editor.getResizeScaleFactor()` (#8042), `TLImageAsset.pixelRatio` (#8163), `sanitizeSvg` (#7896), `experimental__onDropOnCanvas` (#7911), enum-to-const refactoring (#8084) - 6 improvements and 20 bug fixes from production **v4.4.0.mdx:** - Add v4.4.1 patch release section with tooltip positioning fix (#8171) - Add v4.4.1 to keywords **next.mdx:** - Reset with `last_version: v4.5.0` and empty content ### Change type - [x] `other` ### Code changes | Section | LOC change | | ------------- | ------------- | | Documentation | +128 / -107 |


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 (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>: rasterdata:only (nosvg+xml— could embed unsanitized SVG)<use>: fragment refs (#id) only<a>:http:,https:,mailto:onlyCSS sanitization: Strips
@import,expression(),-moz-binding,behavior:, and externalurl()references. Preservesdata:font/image URLs andurl(#id)fragment references (needed for gradients/filters). Handles multilineurl()values (dotAll flag) and CSS escape decoding with bounds checks.URL-bearing presentation attributes: Sanitizes
url()values infill,filter,clip-path,mask,stroke,marker-*,cursor— not justhref/xlink:href. Case-insensitive matching. Externalurl()references in these attributes are stripped whileurl(#id)internal refs are preserved.Animation safety:
<animate>,<set>,<animateColor>,<animateTransform>elements targetinghref,xlink:href, oron*viaattributeNameare removed (prevents runtime injection ofjavascript: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).
maybeSanitizeSvgFilehas 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
improvementTest plan
API changes
sanitizeSvg(svgText: string): string— public export for SDK users building custom handlersRelease notes