/** * Security hardening regression tests. * * Covers: * 1. MCP tool result Unicode sanitization * 2. Sandbox settings source filtering (exclude projectSettings) * 3. Plugin git clone/pull hooks disabled * 4. ANTHROPIC_FOUNDRY_API_KEY removed from SAFE_ENV_VARS * 5. WebFetch SSRF protection via ssrfGuardedLookup */ import { describe, test, expect } from 'bun:test' import { resolve } from 'path' const SRC = resolve(import.meta.dir, '..') const file = (relative: string) => Bun.file(resolve(SRC, relative)) // --------------------------------------------------------------------------- // Fix 1: MCP tool result Unicode sanitization // --------------------------------------------------------------------------- describe('MCP tool result sanitization', () => { test('transformResultContent sanitizes text content', async () => { const content = await file('services/mcp/client.ts').text() // Tool definitions are already sanitized (line ~1798) expect(content).toContain('recursivelySanitizeUnicode(result.tools)') // Tool results must also be sanitized expect(content).toMatch( /case 'text':[\s\S]*?recursivelySanitizeUnicode\(resultContent\.text\)/, ) }) test('resource text content is also sanitized', async () => { const content = await file('services/mcp/client.ts').text() expect(content).toMatch( /recursivelySanitizeUnicode\(\s*`\$\{prefix\}\$\{resource\.text\}`/, ) }) }) // --------------------------------------------------------------------------- // Fix 2: Sandbox settings source filtering // --------------------------------------------------------------------------- describe('Sandbox settings trust boundary', () => { test('getSandboxEnabledSetting does not use getSettings_DEPRECATED', async () => { const content = await file('utils/sandbox/sandbox-adapter.ts').text() // Extract the getSandboxEnabledSetting function body const fnMatch = content.match( /function getSandboxEnabledSetting\(\)[^{]*\{([\s\S]*?)\n\}/, ) expect(fnMatch).not.toBeNull() const fnBody = fnMatch![1] // Must NOT use getSettings_DEPRECATED (reads all sources including project) expect(fnBody).not.toContain('getSettings_DEPRECATED') // Must use getSettingsForSource for individual trusted sources expect(fnBody).toContain("getSettingsForSource('userSettings')") expect(fnBody).toContain("getSettingsForSource('policySettings')") // Must NOT read from projectSettings expect(fnBody).not.toContain("'projectSettings'") }) }) // --------------------------------------------------------------------------- // Fix 3: Plugin git hooks disabled // --------------------------------------------------------------------------- describe('Plugin git operations disable hooks', () => { test('gitClone includes core.hooksPath=/dev/null', async () => { const content = await file('utils/plugins/marketplaceManager.ts').text() // The clone args must disable hooks const cloneSection = content.slice( content.indexOf('export async function gitClone('), content.indexOf('export async function gitClone(') + 2000, ) expect(cloneSection).toContain("'core.hooksPath=/dev/null'") }) test('gitPull includes core.hooksPath=/dev/null', async () => { const content = await file('utils/plugins/marketplaceManager.ts').text() const pullSection = content.slice( content.indexOf('export async function gitPull('), content.indexOf('export async function gitPull(') + 2000, ) expect(pullSection).toContain("'core.hooksPath=/dev/null'") }) test('gitSubmoduleUpdate includes core.hooksPath=/dev/null', async () => { const content = await file('utils/plugins/marketplaceManager.ts').text() const subSection = content.slice( content.indexOf('async function gitSubmoduleUpdate('), content.indexOf('async function gitSubmoduleUpdate(') + 1000, ) expect(subSection).toContain("'core.hooksPath=/dev/null'") }) }) // --------------------------------------------------------------------------- // Fix 4: ANTHROPIC_FOUNDRY_API_KEY not in SAFE_ENV_VARS // --------------------------------------------------------------------------- describe('SAFE_ENV_VARS excludes credentials', () => { test('ANTHROPIC_FOUNDRY_API_KEY is not in SAFE_ENV_VARS', async () => { const content = await file('utils/managedEnvConstants.ts').text() // Extract the SAFE_ENV_VARS set definition const safeStart = content.indexOf('export const SAFE_ENV_VARS') const safeEnd = content.indexOf('])', safeStart) const safeSection = content.slice(safeStart, safeEnd) expect(safeSection).not.toContain('ANTHROPIC_FOUNDRY_API_KEY') }) }) // --------------------------------------------------------------------------- // Fix 5: WebFetch SSRF protection // --------------------------------------------------------------------------- describe('WebFetch SSRF guard', () => { test('getWithPermittedRedirects uses ssrfGuardedLookup', async () => { const content = await file('tools/WebFetchTool/utils.ts').text() expect(content).toContain( "import { ssrfGuardedLookup } from '../../utils/hooks/ssrfGuard.js'", ) // The axios.get call in getWithPermittedRedirects must include lookup const fnSection = content.slice( content.indexOf('export async function getWithPermittedRedirects('), content.indexOf('export async function getWithPermittedRedirects(') + 1000, ) expect(fnSection).toContain('lookup: ssrfGuardedLookup') }) }) // --------------------------------------------------------------------------- // Fix 6: Swarm permission file polling removed (security hardening) // --------------------------------------------------------------------------- describe('Swarm permission file polling removed', () => { test('useSwarmPermissionPoller hook no longer exists', async () => { const content = await file( 'hooks/useSwarmPermissionPoller.ts', ).text() // The file-based polling hook must not exist — it read from an // unauthenticated resolved/ directory where any local process could // forge approval files. expect(content).not.toContain('function useSwarmPermissionPoller(') // The file-based processResponse must not exist expect(content).not.toContain('function processResponse(') }) test('poller does not import from permissionSync', async () => { const content = await file( 'hooks/useSwarmPermissionPoller.ts', ).text() // Must not import anything from permissionSync — all file-based // functions have been removed from this module's dependencies expect(content).not.toContain('permissionSync') }) test('file-based permission functions are marked deprecated', async () => { const content = await file( 'utils/swarm/permissionSync.ts', ).text() // All file-based functions must have @deprecated JSDoc const deprecatedFns = [ 'writePermissionRequest', 'readPendingPermissions', 'readResolvedPermission', 'resolvePermission', 'pollForResponse', 'removeWorkerResponse', ] for (const fn of deprecatedFns) { // Find the function and check that @deprecated appears before it const fnIndex = content.indexOf(`export async function ${fn}(`) if (fnIndex === -1) continue // submitPermissionRequest is a const, not async function const preceding = content.slice(Math.max(0, fnIndex - 500), fnIndex) expect(preceding).toContain('@deprecated') } }) test('mailbox-based functions are NOT deprecated', async () => { const content = await file( 'utils/swarm/permissionSync.ts', ).text() // These are the active path — must not be deprecated const activeFns = [ 'sendPermissionRequestViaMailbox', 'sendPermissionResponseViaMailbox', ] for (const fn of activeFns) { const fnIndex = content.indexOf(`export async function ${fn}(`) expect(fnIndex).not.toBe(-1) const preceding = content.slice(Math.max(0, fnIndex - 300), fnIndex) expect(preceding).not.toContain('@deprecated') } }) })