@marimo-team/islands 0.23.2-dev38 → 0.23.2-dev40

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,5 +1,7 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
- import { describe, expect, it } from "vitest";
2
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
3
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
4
+ import { store } from "@/core/state/jotai";
3
5
  import { isTrustedVirtualFileUrl } from "../trusted-url";
4
6
 
5
7
  describe("isTrustedVirtualFileUrl", () => {
@@ -45,4 +47,46 @@ describe("isTrustedVirtualFileUrl", () => {
45
47
  expect(isTrustedVirtualFileUrl(42)).toBe(false);
46
48
  expect(isTrustedVirtualFileUrl({})).toBe(false);
47
49
  });
50
+
51
+ describe("data URL exemption", () => {
52
+ let previousHasRunAnyCell: boolean;
53
+
54
+ beforeEach(() => {
55
+ previousHasRunAnyCell = store.get(hasRunAnyCellAtom);
56
+ store.set(hasRunAnyCellAtom, true);
57
+ });
58
+
59
+ afterEach(() => {
60
+ store.set(hasRunAnyCellAtom, previousHasRunAnyCell);
61
+ });
62
+
63
+ it.each([
64
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=",
65
+ "data:application/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=",
66
+ "data:text/css;base64,Ym9keXt9",
67
+ ])("accepts safe data URL %s after a cell has run", (url) => {
68
+ expect(isTrustedVirtualFileUrl(url)).toBe(true);
69
+ });
70
+
71
+ it.each([
72
+ // Non-base64 data URLs are refused
73
+ "data:text/javascript,alert(1)",
74
+ "data:text/javascript;charset=utf-8,alert(1)",
75
+ // HTML / SVG / arbitrary types are refused
76
+ "data:text/html;base64,PHNjcmlwdD5hbGVydCgxKTwvc2NyaXB0Pg==",
77
+ "data:image/svg+xml;base64,PHN2Zy8+",
78
+ "data:application/octet-stream;base64,AAA=",
79
+ ])("still rejects unsafe data URL %s", (url) => {
80
+ expect(isTrustedVirtualFileUrl(url)).toBe(false);
81
+ });
82
+
83
+ it("rejects data URL when no cell has been run", () => {
84
+ store.set(hasRunAnyCellAtom, false);
85
+ expect(
86
+ isTrustedVirtualFileUrl(
87
+ "data:text/javascript;base64,ZXhwb3J0IGRlZmF1bHQge30=",
88
+ ),
89
+ ).toBe(false);
90
+ });
91
+ });
48
92
  });
@@ -1,20 +1,45 @@
1
1
  /* Copyright 2026 Marimo. All rights reserved. */
2
2
 
3
+ import { hasRunAnyCellAtom } from "@/components/editor/cell/useRunCells";
4
+ import { store } from "@/core/state/jotai";
5
+
3
6
  /**
4
7
  * Whether a URL can be trusted to point at a marimo-served virtual file.
5
8
  *
6
9
  * Plugins that load remote scripts or stylesheets (e.g. MplInteractive, Panel)
7
10
  * must call this before turning a plugin-supplied URL into a `<script src>` or
8
- * `<link href>`. The backend always serializes these URLs as virtual file
11
+ * `<link href>`. The backend normally serializes these URLs as virtual file
9
12
  * paths of the form `./@file/<byte_length>-<filename>` (see
10
13
  * `VirtualFile.create_and_register`). Accepting anything else would let a
11
14
  * maliciously crafted `<marimo-*>` element embedded in markdown load
12
15
  * attacker-controlled JavaScript at same origin, since the HTML sanitizer
13
16
  * lets arbitrary marimo custom elements and attributes through.
17
+ *
18
+ * Some runtimes (WASM, VS Code) have no backend to serve virtual files, so
19
+ * `VirtualFile` falls back to inline base64 data URLs (see `virtual_file.py`).
20
+ * We accept those only once the user has explicitly run a cell in the current
21
+ * notebook — the same trust signal `sanitize.ts` uses to lift HTML
22
+ * sanitization. Running a cell requires deliberate user action and already
23
+ * executes arbitrary Python, so a data URL script loaded afterwards is not a
24
+ * new attack surface.
14
25
  */
15
26
  export function isTrustedVirtualFileUrl(url: unknown): url is string {
16
27
  if (typeof url !== "string" || url.length === 0) {
17
28
  return false;
18
29
  }
19
- return /^(\.?\/)?@file\/[^?#]+$/.test(url);
30
+ if (/^(\.?\/)?@file\/[^?#]+$/.test(url)) {
31
+ return true;
32
+ }
33
+ if (isSafeDataUrl(url) && store.get(hasRunAnyCellAtom)) {
34
+ return true;
35
+ }
36
+ return false;
37
+ }
38
+
39
+ function isSafeDataUrl(url: string): boolean {
40
+ return (
41
+ url.startsWith("data:text/javascript;base64,") ||
42
+ url.startsWith("data:application/javascript;base64,") ||
43
+ url.startsWith("data:text/css;base64,")
44
+ );
20
45
  }