diff --git a/README.md b/README.md index 9ab205e..55cc05f 100644 --- a/README.md +++ b/README.md @@ -677,6 +677,10 @@ Shannon is designed for legitimate security auditing purposes only. Windows Defender may flag files in `xben-benchmark-results/` or `deliverables/` as malware. These are false positives caused by exploit code in the reports. Add an exclusion for the Shannon directory in Windows Defender, or use Docker/WSL2. +#### **7. Security Considerations** + +Shannon Lite is designed for scanning repositories and applications you own or have explicit permission to test. Do not point it at untrusted or adversarial codebases. Like any AI-powered tool that reads source code, Shannon Lite is susceptible to prompt injection from content in the scanned repository. + ## 📜 License diff --git a/docker-compose.yml b/docker-compose.yml index 843e752..eede388 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -3,8 +3,8 @@ services: image: temporalio/temporal:latest command: ["server", "start-dev", "--db-filename", "/home/temporal/temporal.db", "--ip", "0.0.0.0"] ports: - - "7233:7233" # gRPC - - "8233:8233" # Web UI (built-in) + - "127.0.0.1:7233:7233" # gRPC + - "127.0.0.1:8233:8233" # Web UI (built-in) volumes: - temporal-data:/home/temporal healthcheck: @@ -47,7 +47,6 @@ services: - ./repos:/repos - ${BENCHMARKS_BASE:-.}:/benchmarks shm_size: 2gb - ipc: host security_opt: - seccomp:unconfined @@ -63,7 +62,7 @@ services: envsubst < /config/router-config.json > /root/.claude-code-router/config.json && ccr start" ports: - - "3456:3456" + - "127.0.0.1:3456:3456" volumes: - ./configs/router-config.json:/config/router-config.json:ro environment: diff --git a/src/ai/claude-executor.ts b/src/ai/claude-executor.ts index 04c990c..3f7fd0c 100644 --- a/src/ai/claude-executor.ts +++ b/src/ai/claude-executor.ts @@ -82,7 +82,7 @@ function buildMcpServers( const isDocker = process.env.SHANNON_DOCKER === 'true'; const mcpArgs: string[] = [ - '@playwright/mcp@latest', + '@playwright/mcp@0.0.68', '--isolated', '--user-data-dir', userDataDir, ]; @@ -92,13 +92,29 @@ function buildMcpServers( mcpArgs.push('--browser', 'chromium'); } - const envVars: Record = Object.fromEntries( - Object.entries({ - ...process.env, - PLAYWRIGHT_HEADLESS: 'true', - ...(isDocker && { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' }), - }).filter((entry): entry is [string, string] => entry[1] !== undefined) - ); + // NOTE: Explicit allowlist — the Playwright MCP subprocess must not inherit + // secrets (API keys, AWS tokens) from the parent process. + const MCP_ENV_ALLOWLIST = [ + 'PATH', 'HOME', 'NODE_PATH', 'DISPLAY', + 'PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH', + ] as const; + + const envVars: Record = { + PLAYWRIGHT_HEADLESS: 'true', + ...(isDocker && { PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: '1' }), + }; + + for (const key of MCP_ENV_ALLOWLIST) { + if (process.env[key]) { + envVars[key] = process.env[key]!; + } + } + + for (const [key, value] of Object.entries(process.env)) { + if (key.startsWith('XDG_') && value !== undefined) { + envVars[key] = value; + } + } mcpServers[playwrightMcpName] = { type: 'stdio' as const, diff --git a/src/services/prompt-manager.ts b/src/services/prompt-manager.ts index b660dc8..b852c73 100644 --- a/src/services/prompt-manager.ts +++ b/src/services/prompt-manager.ts @@ -102,10 +102,19 @@ async function buildLoginInstructions(authentication: Authentication, logger: Ac // Pure function: Process @include() directives async function processIncludes(content: string, baseDir: string): Promise { const includeRegex = /@include\(([^)]+)\)/g; - // Use a Promise.all to handle all includes concurrently + const resolvedBase = path.resolve(baseDir); + const replacements: IncludeReplacement[] = await Promise.all( Array.from(content.matchAll(includeRegex)).map(async (match) => { - const includePath = path.join(baseDir, match[1]!); + const includePath = path.resolve(baseDir, match[1]!); + if (!includePath.startsWith(resolvedBase + path.sep) && includePath !== resolvedBase) { + throw new PentestError( + `Path traversal detected in @include(): ${match[1]}`, + 'prompt', + false, + { includePath, baseDir: resolvedBase } + ); + } const sharedContent = await fs.readFile(includePath, 'utf8'); return { placeholder: match[0],