Skip to content

fix: sanitize marimo-mpl-interactive marimo-panel#9133

Merged
dmadisetti merged 2 commits into
mainfrom
ms/sanitize-mpl
Apr 10, 2026
Merged

fix: sanitize marimo-mpl-interactive marimo-panel#9133
dmadisetti merged 2 commits into
mainfrom
ms/sanitize-mpl

Conversation

@mscolnick

Copy link
Copy Markdown
Contributor

No description provided.

@vercel

vercel Bot commented Apr 10, 2026

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
marimo-docs Ready Ready Preview, Comment Apr 10, 2026 7:13pm

Request Review

Copilot AI review requested due to automatic review settings April 10, 2026 17:32
@mscolnick mscolnick requested review from dmadisetti and removed request for Light2Dark and manzt April 10, 2026 17:33
@codecov

codecov Bot commented Apr 10, 2026

Copy link
Copy Markdown

Bundle Report

Changes will increase total bundle size by 12.21kB (0.05%) ⬆️. This is within the configured threshold ✅

Detailed changes
Bundle name Size Change
marimo-esm 24.84MB 12.21kB (0.05%) ⬆️

Affected Assets, Files, and Routes:

view changes for bundle: marimo-esm

Assets Changed:

Asset Name Size Change Total Size Change (%)
assets/cells-*.js 384 bytes 703.39kB 0.05%
assets/index-*.js 91 bytes 601.56kB 0.02%
assets/index-*.css 290 bytes 362.29kB 0.08%
assets/dist-*.js 92 bytes 256 bytes 56.1% ⚠️
assets/dist-*.js 122 bytes 259 bytes 89.05% ⚠️
assets/dist-*.js -33 bytes 104 bytes -24.09%
assets/dist-*.js 243 bytes 403 bytes 151.88% ⚠️
assets/dist-*.js -152 bytes 183 bytes -45.37%
assets/dist-*.js -46 bytes 137 bytes -25.14%
assets/dist-*.js 107 bytes 276 bytes 63.31% ⚠️
assets/dist-*.js 231 bytes 335 bytes 222.12% ⚠️
assets/dist-*.js -17 bytes 160 bytes -9.6%
assets/dist-*.js -83 bytes 176 bytes -32.05%
assets/dist-*.js -266 bytes 137 bytes -66.0%
assets/dist-*.js -79 bytes 104 bytes -43.17%
assets/dist-*.js 79 bytes 183 bytes 75.96% ⚠️
assets/dist-*.js 60 bytes 164 bytes 57.69% ⚠️
assets/dist-*.js -285 bytes 102 bytes -73.64%
assets/dist-*.js -107 bytes 169 bytes -38.77%
assets/dist-*.js 2 bytes 104 bytes 1.96%
assets/dist-*.js 1 bytes 177 bytes 0.57%
assets/dist-*.js 131 bytes 387 bytes 51.17% ⚠️
assets/JsonOutput-*.js -1 bytes 336.29kB -0.0%
assets/edit-*.js -42.36kB 329.61kB -11.39%
assets/layout-*.js 56.26kB 185.91kB 43.39% ⚠️
assets/slides-*.js 360 bytes 116.37kB 0.31%
assets/panels-*.js -3.15kB 45.36kB -6.5%
assets/state-*.js 192 bytes 24.05kB 0.8%
assets/RenderHTML-*.js 60 bytes 5.01kB 1.21%
assets/button-*.js 93 bytes 3.93kB 2.43%
assets/__vite-*.js 5 bytes 98 bytes 5.38% ⚠️
assets/__vite-*.js -5 bytes 93 bytes -5.1%

Files in assets/index-*.js:

  • ./src/plugins/impl/panel/PanelPlugin.tsx → Total Size: 5.86kB

  • ./src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx → Total Size: 6.71kB

Files in assets/state-*.js:

  • ./src/plugins/impl/anywidget/widget-binding.ts → Total Size: 4.8kB

  • ./src/plugins/core/trusted-url.ts → Total Size: 861 bytes

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

This PR hardens plugin script/style loading to mitigate same-origin script injection via raw <marimo-*> elements embedded in markdown, by ensuring extensions are served from Marimo virtual files and validating URLs on the frontend.

Changes:

  • Serve Panel’s extension JavaScript via a ./@file/... virtual file URL instead of inlining it in the DOM.
  • Add a shared frontend isTrustedVirtualFileUrl validator and apply it to Panel, mpl-interactive, and anywidget loaders.
  • Add targeted unit tests (Python + Vitest) covering the new URL validation and the Panel virtual-file attribute.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
tests/_plugins/ui/_impl/test_panel.py Adds regression test ensuring Panel extension is referenced via data-extension-url and points at @file/.
marimo/_plugins/ui/_impl/from_panel.py Switches Panel extension delivery from inline JS to a registered VirtualFile URL.
frontend/src/plugins/impl/panel/PanelPlugin.tsx Loads Panel extension via <script src> and refuses non-virtual-file URLs.
frontend/src/plugins/impl/panel/tests/PanelPlugin.test.ts Tests trusted vs untrusted extension URLs for Panel.
frontend/src/plugins/impl/mpl-interactive/MplInteractivePlugin.tsx Refuses untrusted mpl.js/CSS URLs and exposes internals for testing.
frontend/src/plugins/impl/mpl-interactive/tests/MplInteractivePlugin.test.tsx Tests URL refusal/acceptance behavior for mpl-interactive asset loading.
frontend/src/plugins/impl/anywidget/widget-binding.ts Refuses untrusted dynamic-import URLs for anywidget modules.
frontend/src/plugins/impl/anywidget/tests/widget-binding.test.ts Adds URL validation tests for anywidget registry imports.
frontend/src/plugins/core/trusted-url.ts Introduces shared “trusted virtual file URL” validator.
frontend/src/plugins/core/test/trusted-url.test.ts Unit tests for trusted/untrusted virtual file URL patterns.

if (typeof url !== "string" || url.length === 0) {
return false;
}
return /^(\.?\/)?@file\/[^?#]+$/.test(url);

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

The current regex allows additional "/" characters after the @file/ prefix (e.g. "./@file/../auth/token"). Browsers normalize dot-segments in URLs, so this can resolve outside the virtual-file route and reintroduce same-origin script/style injection. Tighten validation so the portion after "@file/" is a single path segment (no "/"), and consider rejecting percent-encoded slashes/dot-segments; add a unit test for the "./@file/../..." case.

Suggested change
return /^(\.?\/)?@file\/[^?#]+$/.test(url);
// Reject percent-encoded separators / dot-segments that browsers may
// normalize into paths outside the virtual-file route.
if (/%2f|%5c|%2e/i.test(url)) {
return false;
}
// Only allow a single path segment after "@file/".
return /^(\.?\/)?@file\/[^\/\\?#]+$/.test(url);

Copilot uses AI. Check for mistakes.
Comment thread marimo/_plugins/ui/_impl/from_panel.py Outdated
Comment on lines +316 to +321
extension_url: Optional[str] = None
if extension:
extension_vfile = VirtualFile.create_and_register(
extension.encode("utf-8"), "js"
)
extension_url = extension_vfile.url

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

VirtualFile.create_and_register() registers the extension script in the runtime virtual-file registry but there’s no corresponding cleanup or caching here. Because VirtualFile filenames are randomized, rerunning cells / creating multiple Panel widgets can accumulate many large JS virtual files in storage over a long session. Consider caching the extension VirtualFile (e.g. module-level/lru_cache keyed by the extension contents) and/or using a lifecycle-managed virtual file mechanism so these entries don’t grow without bound.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +134
}
const script = document.createElement("script");
script.src = extensionUrl;

Copilot AI Apr 10, 2026

Copy link

Choose a reason for hiding this comment

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

loadPanelExtension() appends a new <script> tag every time it’s called, even if the same extension URL was already loaded. If multiple panel elements mount (or a cell re-runs), this can duplicate network fetches and re-execute the extension script unnecessarily. Consider deduplicating by checking for an existing script with the same src (or checking a global “extension loaded” marker) before appending.

Suggested change
}
const script = document.createElement("script");
script.src = extensionUrl;
}
const script = document.createElement("script");
script.src = extensionUrl;
const normalizedSrc = script.src;
const existingScript = Array.from(document.scripts).find(
(existing) => existing.src === normalizedSrc,
);
if (existingScript) {
return true;
}

Copilot uses AI. Check for mistakes.
@dmadisetti dmadisetti added the bug Something isn't working label Apr 10, 2026
@dmadisetti dmadisetti merged commit 1ee86d6 into main Apr 10, 2026
35 of 43 checks passed
@dmadisetti dmadisetti deleted the ms/sanitize-mpl branch April 10, 2026 19:18
@dmadisetti dmadisetti restored the ms/sanitize-mpl branch April 10, 2026 19:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants