From 45934aa634c8342c8a0c877dc432fa83c74f6f07 Mon Sep 17 00:00:00 2001 From: Al Marks Date: Wed, 12 Aug 2026 11:29:04 -0700 Subject: [PATCH] Verify that blob urls are blob: or data: scheme before fetching --- packages/visual-editor/src/data/common.ts | 3 + .../visual-editor/tests/data/as-blob.test.ts | 141 ++++++++++++++++++ 2 files changed, 144 insertions(+) create mode 100644 packages/visual-editor/tests/data/as-blob.test.ts diff --git a/packages/visual-editor/src/data/common.ts b/packages/visual-editor/src/data/common.ts index 845dff69b48..d5adea29a1c 100644 --- a/packages/visual-editor/src/data/common.ts +++ b/packages/visual-editor/src/data/common.ts @@ -119,6 +119,9 @@ export const asBlob = async ( let url: string; if (isStoredData(part)) { url = part.storedData.handle; + if (!url.startsWith("blob:") && !url.startsWith("data:")) { + throw new Error("Invalid stored data URL"); + } } else if (isChunk(part)) { const { mimetype } = part; let { data } = part; diff --git a/packages/visual-editor/tests/data/as-blob.test.ts b/packages/visual-editor/tests/data/as-blob.test.ts new file mode 100644 index 00000000000..d3dc607a948 --- /dev/null +++ b/packages/visual-editor/tests/data/as-blob.test.ts @@ -0,0 +1,141 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, mock, beforeEach, afterEach } from "node:test"; +import assert from "node:assert/strict"; +import { asBlob } from "../../src/data/common.js"; + +describe("asBlob", () => { + beforeEach(() => { + mock.restoreAll(); + }); + + afterEach(() => { + mock.restoreAll(); + }); + + it("rejects a relative URL handle", async () => { + const fakeFetch = mock.fn(async () => new Response("data")); + mock.method(globalThis, "fetch", fakeFetch); + + const part = { + storedData: { + handle: "/path/to/resource", + mimeType: "application/json", + }, + }; + + await assert.rejects( + () => asBlob(part), + (error: Error) => { + assert.ok( + error.message.length > 0, + "Expected a validation error message" + ); + return true; + }, + "asBlob should reject storedData handles that are relative URLs" + ); + + assert.equal( + fakeFetch.mock.callCount(), + 0, + "fetch must not be called for a relative URL handle" + ); + }); + + it("rejects an https: URL handle", async () => { + const fakeFetch = mock.fn(async () => new Response("data")); + mock.method(globalThis, "fetch", fakeFetch); + + const part = { + storedData: { + handle: "https://example.com/data", + mimeType: "application/json", + }, + }; + + await assert.rejects( + () => asBlob(part), + (error: Error) => { + assert.ok( + error.message.length > 0, + "Expected a validation error message" + ); + return true; + }, + "asBlob should reject storedData handles with https: scheme" + ); + + assert.equal( + fakeFetch.mock.callCount(), + 0, + "fetch must not be called for an https: storedData handle" + ); + }); + + it("rejects an http: URL handle", async () => { + const fakeFetch = mock.fn(async () => new Response("data")); + mock.method(globalThis, "fetch", fakeFetch); + + const part = { + storedData: { + handle: "http://example.com/data", + mimeType: "application/json", + }, + }; + + await assert.rejects( + () => asBlob(part), + (error: Error) => { + assert.ok( + error.message.length > 0, + "Expected a validation error message" + ); + return true; + }, + "asBlob should reject storedData handles with http: scheme" + ); + + assert.equal( + fakeFetch.mock.callCount(), + 0, + "fetch must not be called for an http: storedData handle" + ); + }); + + it("fetches a valid blob: URL handle", async () => { + const fakeFetch = mock.fn(async () => new Response("blob-data")); + mock.method(globalThis, "fetch", fakeFetch); + + const part = { + storedData: { + handle: "blob:http://localhost/1234-5678", + mimeType: "image/png", + }, + }; + + const blob = await asBlob(part); + assert.ok(blob); + assert.equal(fakeFetch.mock.callCount(), 1); + }); + + it("fetches a valid data: URL handle", async () => { + const fakeFetch = mock.fn(async () => new Response("data-content")); + mock.method(globalThis, "fetch", fakeFetch); + + const part = { + storedData: { + handle: "data:text/plain;base64,SGVsbG8=", + mimeType: "text/plain", + }, + }; + + const blob = await asBlob(part); + assert.ok(blob); + assert.equal(fakeFetch.mock.callCount(), 1); + }); +});