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:
parent
9f8dc82a9a
commit
45785a0aa6
1 changed files with 133 additions and 13 deletions
|
|
@ -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({});
|
||||
|
|
|
|||
Loading…
Reference in a new issue