fix(web): screen app-supplied sandbox permissions before granting them

The sandbox proxy's buildAllowAttribute() tests each permission key for
truthiness, so an untrusted `_meta.ui.permissions` bag could stand a
non-boolean in for a grant — `{ camera: "false" }`, a plausible way for a
server to mean "off", reads as on. Nothing screened it, because before
the `_meta.ui` fix no permissions reached the proxy at all.

Add approveSandboxPermissions alongside approveCspSources: keep only the
four keys the proxy knows, and only where the value is the spec's empty-
object marker or `true`; drop anything else (warning on a malformed
value, silently on an explicit `false`) and return undefined when nothing
survives. Both the notification and the hostCapabilities echo now carry
the approved set, so the app sees what was granted.

Also document what the listing lookup can and cannot see under
`paginatedLists`.

Signed-off-by: cliffhall <cliff@futurescale.com>
This commit is contained in:
cliffhall 2026-08-23 21:22:14 -04:00
commit 45785a0aa6

View file

@ -2,10 +2,12 @@ import { describe, expect, it, vi } from "vitest";
import {
SAFE_CSP_SOURCE,
approveCspSources,
approveSandboxPermissions,
buildSandboxCspPolicy,
escapeHtmlAttr,
wrapSandboxedHtml,
} from "./sandbox-csp";
import type { McpUiResourcePermissions } from "@modelcontextprotocol/ext-apps/app-bridge";
describe("SAFE_CSP_SOURCE", () => {
it.each([
@ -81,6 +83,52 @@ describe("approveCspSources", () => {
});
});
describe("approveSandboxPermissions", () => {
it("keeps the spec's empty-object marker and `true` as shorthand", () => {
expect(
approveSandboxPermissions({
camera: {},
microphone: true as unknown as Record<string, never>,
}),
).toEqual({ camera: {}, microphone: {} });
});
it("drops a truthy non-marker value the proxy would have honored", () => {
// The proxy's buildAllowAttribute() tests each key for truthiness, so the
// string "false" — a plausible way for a server to mean "off" — would
// otherwise switch the camera on.
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(
approveSandboxPermissions({
camera: "false",
geolocation: 1,
microphone: null,
} as unknown as McpUiResourcePermissions),
).toBeUndefined();
expect(warn).toHaveBeenCalledTimes(3);
warn.mockRestore();
});
it("drops `false` without warning, and ignores unknown keys", () => {
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});
expect(
approveSandboxPermissions({
camera: false,
midi: {},
clipboardWrite: {},
} as unknown as McpUiResourcePermissions),
).toEqual({ clipboardWrite: {} });
// `false` is an explicit refusal, not a malformed value.
expect(warn).not.toHaveBeenCalled();
warn.mockRestore();
});
it("returns undefined for no permissions at all", () => {
expect(approveSandboxPermissions(undefined)).toBeUndefined();
expect(approveSandboxPermissions({})).toBeUndefined();
});
});
describe("buildSandboxCspPolicy", () => {
it("emits locked-down defaults for an empty approved csp", () => {
const policy = buildSandboxCspPolicy({});