From 239e842790eeb7183282feff1bdd640ef192bc1f Mon Sep 17 00:00:00 2001 From: Mario Zechner Date: Wed, 22 Apr 2026 19:14:11 +0200 Subject: [PATCH] fix(web-ui): sandbox svg artifact previews closes #3552 --- packages/web-ui/CHANGELOG.md | 4 ++ .../web-ui/src/tools/artifacts/SvgArtifact.ts | 45 ++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/packages/web-ui/CHANGELOG.md b/packages/web-ui/CHANGELOG.md index efe7feab5..c8d1b31a1 100644 --- a/packages/web-ui/CHANGELOG.md +++ b/packages/web-ui/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Render SVG artifact previews through a blob-backed image instead of injecting untrusted SVG markup into the page DOM ([#3552](https://github.com/badlogic/pi-mono/issues/3552)) + ## [0.68.1] - 2026-04-22 ## [0.68.0] - 2026-04-20 diff --git a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts index 6901192e9..fe9f398f6 100644 --- a/packages/web-ui/src/tools/artifacts/SvgArtifact.ts +++ b/packages/web-ui/src/tools/artifacts/SvgArtifact.ts @@ -13,11 +13,17 @@ export class SvgArtifact extends ArtifactElement { @property() override filename = ""; private _content = ""; + @state() private previewUrl = ""; + override get content(): string { return this._content; } override set content(value: string) { + if (this._content === value) { + return; + } this._content = value; + this.updatePreviewUrl(); this.requestUpdate(); } @@ -31,6 +37,21 @@ export class SvgArtifact extends ArtifactElement { this.viewMode = mode; } + private revokePreviewUrl() { + if (this.previewUrl) { + URL.revokeObjectURL(this.previewUrl); + this.previewUrl = ""; + } + } + + private updatePreviewUrl() { + this.revokePreviewUrl(); + if (!this._content) { + return; + } + this.previewUrl = URL.createObjectURL(new Blob([this._content], { type: "image/svg+xml" })); + } + public getHeaderButtons() { const toggle = new PreviewCodeToggle(); toggle.mode = this.viewMode; @@ -52,14 +73,34 @@ export class SvgArtifact extends ArtifactElement { `; } + override connectedCallback() { + super.connectedCallback(); + if (this._content && !this.previewUrl) { + this.updatePreviewUrl(); + } + } + + override disconnectedCallback() { + super.disconnectedCallback(); + this.revokePreviewUrl(); + } + override render() { return html`
${ this.viewMode === "preview" - ? html`
- ${unsafeHTML(this.content.replace(/)/i, (_m, p1) => ` + ${ + this.previewUrl + ? html`${this.filename}` + : "" + }
` : html`
${unsafeHTML(
 									hljs.highlight(this.content, { language: "xml", ignoreIllegals: true }).value,