Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/**
* `__sdk_dispatch` is a guest-visible global; a malicious script can call it
* directly with an un-manifested method, a poisoned `orgPath`, or a cross-org
* path the manifest never advertised. The host must re-enforce the manifest,
* the org-slug guard, and `allowCrossOrg` at dispatch time — regardless of mode.
*/

import { describe, expect, it } from "bun:test";
import type { ClientSDK } from "../../../sandbox/client-sdk";
import { runOrSkip, stubSDK } from "./_helpers";

const DISPATCH = (path: string, args: unknown[], orgPath: string[] = []) =>
[
"export default async () => {",
` const payload = JSON.stringify({ args: ${JSON.stringify(args)}, orgPath: ${JSON.stringify(orgPath)} });`,
` const r = await __sdk_dispatch.apply(undefined, [${JSON.stringify(path)}, payload], { result: { promise: true, copy: true } });`,
" return r === undefined ? null : JSON.parse(r);",
"};",
].join("\n");

describe("host-side manifest enforcement on __sdk_dispatch", () => {
it("rejects an un-manifested write in run_sdk mode even though the SDK exposes it", async () => {
let called = false;
const sdk = stubSDK({
entities: {
// Not present in METHOD_METADATA → never advertised by the manifest.
wipeEverything: async () => {
called = true;
return { wiped: true };
},
list: async () => ({ entities: [] }),
} as never,
});
const result = await runOrSkip({
source: DISPATCH("entities.wipeEverything", [{}]),
sdk,
sdkMode: "full",
});
if (!result) return;
expect(result.success).toBe(false);
expect(result.error?.message).toMatch(/Unknown SDK method/);
expect(called).toBe(false);
});

it("rejects a __proto__ org slug before calling target.org", async () => {
let orgCalls = 0;
const sdk = stubSDK({
org: async (slug: string): Promise<ClientSDK> => {
orgCalls += 1;
return stubSDK({ entities: { list: async () => ({ entities: [] }) } as never, _slug: slug } as never);
},
entities: { list: async () => ({ entities: [] }) } as never,
});
const result = await runOrSkip({
source: DISPATCH("entities.list", [{}], ["__proto__"]),
sdk,
sdkMode: "full",
allowCrossOrg: true,
});
if (!result) return;
expect(result.success).toBe(false);
expect(result.error?.message).toMatch(/Invalid org slug/);
expect(orgCalls).toBe(0);
});

it("rejects a non-empty orgPath when allowCrossOrg is false", async () => {
let orgCalls = 0;
const sdk = stubSDK({
org: async (): Promise<ClientSDK> => {
orgCalls += 1;
return stubSDK();
},
entities: { list: async () => ({ entities: [] }) } as never,
});
const result = await runOrSkip({
source: DISPATCH("entities.list", [{}], ["other-org"]),
sdk,
sdkMode: "full",
allowCrossOrg: false,
});
if (!result) return;
expect(result.success).toBe(false);
expect(result.error?.message).toMatch(/CrossOrgAccessDenied/);
expect(orgCalls).toBe(0);
});

it("still allows a manifested call", async () => {
const sdk = stubSDK({
entities: { list: async () => ({ entities: [{ id: 1 }] }) } as never,
});
const result = await runOrSkip({
source: DISPATCH("entities.list", [{}]),
sdk,
sdkMode: "full",
});
if (!result) return;
expect(result.success).toBe(true);
expect(result.returnValue).toEqual({ entities: [{ id: 1 }] } as never);
});
});
39 changes: 35 additions & 4 deletions packages/server/src/sandbox/run-script.ts
Original file line number Diff line number Diff line change
Expand Up @@ -276,9 +276,19 @@ export async function runScript(
const started = Date.now();
const limits = clampLimits(options.limits);
const sdkMode: SDKMode = options.sdkMode ?? "full";
const manifest = enumerateSDKManifest(sdkMode, {
allowCrossOrg: options.allowCrossOrg ?? false,
});
const allowCrossOrg = options.allowCrossOrg ?? false;
const manifest = enumerateSDKManifest(sdkMode, { allowCrossOrg });

// Host-side mirror of the manifest's dispatchable paths. `__sdk_dispatch` is a
// guest-visible global, so a malicious script can call it with an un-manifested
// method directly — the guest-side Proxy filter is the friendly path, this is
// the security backstop. `org` is a guest-side construct (re-walks `orgPath`),
// never a dispatch path, so it isn't in this set.
const allowedDispatchPaths = new Set<string>(["log", "query"]);
for (const [ns, methods] of Object.entries(manifest.byNamespace)) {
for (const method of methods) allowedDispatchPaths.add(`${ns}.${method}`);
}
const FORBIDDEN_ORG_SLUGS = new Set(["__proto__", "constructor", "prototype"]);

// Wall-clock timeout. Each dispatch races the abort signal so the script
// returns promptly; upstream DB/HTTP itself doesn't cancel today.
Expand Down Expand Up @@ -376,8 +386,29 @@ export async function runScript(
orgPath: string[];
};

// Re-enforce the manifest on the host: reject any method the manifest
// wouldn't advertise, regardless of run mode (dry-run only skips writes
// for the modes that have it — it is not an authorization gate).
if (!allowedDispatchPaths.has(path)) {
throw new Error(`Unknown SDK method: '${path}'`);
}
// Cross-org access is gated here too, not just by the manifest omitting
// `org` from `topLevel`.
if (!allowCrossOrg && orgPath.length > 0) {
throw new Error("CrossOrgAccessDenied: cross-org access is not available here.");
}

let target: ClientSDK = baseSdk;
for (const slug of orgPath) target = await target.org(slug);
for (const slug of orgPath) {
if (
typeof slug !== "string" ||
FORBIDDEN_ORG_SLUGS.has(slug) ||
!Object.prototype.hasOwnProperty.call(target, "org")
) {
throw new Error(`Invalid org slug: '${String(slug)}'`);
}
target = await target.org(slug);
}

const dispatchPromise: Promise<unknown> = (async () => {
if (path === "log") {
Expand Down
Loading