From be6d7539d64fc3713125d7619c72871d38458ea1 Mon Sep 17 00:00:00 2001 From: Pranshul Soni Date: Sat, 5 Sep 2026 14:01:58 +0530 Subject: [PATCH] config: reject unknown top-level keys in SandboxRuntimeConfigSchema The top-level schema was the only non-strict one in sandbox-config.ts. A typo like 'denyWriteTypo' loaded without warning and was silently stripped, so an operator could believe a policy was enforced when no restriction existed (issue #434). Apply .strict() for parity with the sub-schemas, which already reject unknown keys for exactly this reason (see the credentials.injectHosts comment). loadConfig's existing error rendering prints each issue, so the operator now sees the unrecognized key name. --- src/sandbox/sandbox-config.ts | 4 +++ test/config-validation.test.ts | 55 ++++++++++++++++++++++++++-------- 2 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/sandbox/sandbox-config.ts b/src/sandbox/sandbox-config.ts index baa6052d..df21ba6a 100644 --- a/src/sandbox/sandbox-config.ts +++ b/src/sandbox/sandbox-config.ts @@ -1140,6 +1140,10 @@ export const SandboxRuntimeConfigSchema = z '`safe.directory` without adding them to `filesystem.allowWrite`.', ), }) + // Reject unknown top-level keys so a typo like `denyWriteTypo` fails + // loudly instead of being silently stripped while the operator believes + // the policy is enforced. Same reasoning as the strict sub-schemas above. + .strict() .superRefine((cfg, ctx) => { const creds = cfg.credentials if (!creds) return diff --git a/test/config-validation.test.ts b/test/config-validation.test.ts index a041958b..c6ec63db 100644 --- a/test/config-validation.test.ts +++ b/test/config-validation.test.ts @@ -2,20 +2,20 @@ import { describe, test, expect } from 'bun:test' import { SandboxRuntimeConfigSchema } from '../src/sandbox/sandbox-config.js' describe('Config Validation', () => { - test('should validate a valid minimal config', () => { - const config = { - network: { - allowedDomains: [], - deniedDomains: [], - }, - filesystem: { - denyRead: [], - allowWrite: [], - denyWrite: [], - }, - } + const validConfig = { + network: { + allowedDomains: [], + deniedDomains: [], + }, + filesystem: { + denyRead: [], + allowWrite: [], + denyWrite: [], + }, + } - const result = SandboxRuntimeConfigSchema.safeParse(config) + test('should validate a valid minimal config', () => { + const result = SandboxRuntimeConfigSchema.safeParse(validConfig) expect(result.success).toBe(true) }) @@ -127,6 +127,35 @@ describe('Config Validation', () => { expect(result.success).toBe(false) }) + test('should reject unknown top-level keys', () => { + // A typo like `denyWriteTypo` must fail loudly instead of being + // silently stripped while the operator believes the policy is + // enforced (issue #434). + const config = { + ...validConfig, + denyWriteTypo: ['/Users/me'], + } + + const result = SandboxRuntimeConfigSchema.safeParse(config) + expect(result.success).toBe(false) + if (!result.success) { + const messages = result.error.issues.map(issue => issue.message) + expect(messages.some(message => message.includes('denyWriteTypo'))).toBe( + true, + ) + } + }) + + test('should still reject unknown top-level keys alongside valid config', () => { + const config = { + ...validConfig, + allowEverything: true, + } + + const result = SandboxRuntimeConfigSchema.safeParse(config) + expect(result.success).toBe(false) + }) + test('should validate wildcard domains correctly', () => { const validWildcards = ['*.example.com', '*.github.io', '*.co.uk']