diff --git a/examples/basic-curl.sh b/examples/basic-curl.sh old mode 100644 new mode 100755 diff --git a/src/docker-manager.test.ts b/src/docker-manager.test.ts index c893353d..9ef80eff 100644 --- a/src/docker-manager.test.ts +++ b/src/docker-manager.test.ts @@ -1495,6 +1495,29 @@ describe('docker-manager', () => { expect(fs.existsSync(path.join(testDir, 'squid-logs'))).toBe(true); }); + it('should create /tmp/gh-aw/mcp-logs directory with world-writable permissions', async () => { + const config: WrapperConfig = { + allowedDomains: ['github.com'], + agentCommand: 'echo test', + logLevel: 'info', + keepContainers: false, + workDir: testDir, + }; + + try { + await writeConfigs(config); + } catch { + // May fail, but directory should still be created + } + + // Verify /tmp/gh-aw/mcp-logs directory was created + expect(fs.existsSync('/tmp/gh-aw/mcp-logs')).toBe(true); + const stats = fs.statSync('/tmp/gh-aw/mcp-logs'); + expect(stats.isDirectory()).toBe(true); + // Verify permissions are 0o777 (rwxrwxrwx) to allow non-root users to create subdirectories + expect((stats.mode & 0o777).toString(8)).toBe('777'); + }); + it('should write squid.conf file', async () => { const config: WrapperConfig = { allowedDomains: ['github.com', 'example.com'], diff --git a/src/docker-manager.ts b/src/docker-manager.ts index 61b9c5fc..22f517ea 100644 --- a/src/docker-manager.ts +++ b/src/docker-manager.ts @@ -718,6 +718,17 @@ export function generateDockerCompose( dns_search: [], // Disable DNS search domains to prevent embedded DNS fallback volumes: agentVolumes, environment, + // Hide /tmp/gh-aw/mcp-logs directory using tmpfs (empty in-memory filesystem) + // This prevents the agent from accessing MCP server logs while still allowing + // the host to write logs to /tmp/gh-aw/mcp-logs/ (e.g., /tmp/gh-aw/mcp-logs/safeoutputs/) + // For normal mode: hide /tmp/gh-aw/mcp-logs + // For chroot mode: hide both /tmp/gh-aw/mcp-logs and /host/tmp/gh-aw/mcp-logs + tmpfs: config.enableChroot + ? [ + '/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m', + '/host/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m', + ] + : ['/tmp/gh-aw/mcp-logs:rw,noexec,nosuid,size=1m'], depends_on: { 'squid-proxy': { condition: 'service_healthy', @@ -861,9 +872,28 @@ export async function writeConfigs(config: WrapperConfig): Promise { const squidLogsDir = config.proxyLogsDir || path.join(config.workDir, 'squid-logs'); if (!fs.existsSync(squidLogsDir)) { fs.mkdirSync(squidLogsDir, { recursive: true, mode: 0o777 }); + // Explicitly set permissions to 0o777 (not affected by umask) + fs.chmodSync(squidLogsDir, 0o777); } logger.debug(`Squid logs directory created at: ${squidLogsDir}`); + // Create /tmp/gh-aw/mcp-logs directory + // This directory exists on the HOST for MCP gateway to write logs + // Inside the AWF container, it's hidden via tmpfs mount (see generateDockerCompose) + // Uses mode 0o777 to allow GitHub Actions workflows and MCP gateway to create subdirectories + // even when AWF runs as root (e.g., sudo awf --enable-chroot) + const mcpLogsDir = '/tmp/gh-aw/mcp-logs'; + if (!fs.existsSync(mcpLogsDir)) { + fs.mkdirSync(mcpLogsDir, { recursive: true, mode: 0o777 }); + // Explicitly set permissions to 0o777 (not affected by umask) + fs.chmodSync(mcpLogsDir, 0o777); + logger.debug(`MCP logs directory created at: ${mcpLogsDir}`); + } else { + // Fix permissions if directory already exists (e.g., created by a previous run) + fs.chmodSync(mcpLogsDir, 0o777); + logger.debug(`MCP logs directory permissions fixed at: ${mcpLogsDir}`); + } + // Use fixed network configuration (network is created by host-iptables.ts) const networkConfig = { subnet: '172.30.0.0/24', diff --git a/tests/integration/credential-hiding.test.ts b/tests/integration/credential-hiding.test.ts index 8c332903..a53f60ef 100644 --- a/tests/integration/credential-hiding.test.ts +++ b/tests/integration/credential-hiding.test.ts @@ -294,4 +294,60 @@ describe('Credential Hiding Security', () => { expect(output).not.toContain('auth'); }, 120000); }); + + describe('MCP Logs Directory Hiding', () => { + test('Test 13: /tmp/gh-aw/mcp-logs/ is hidden in normal mode', async () => { + // Try to access the mcp-logs directory + const result = await runner.runWithSudo( + 'ls -la /tmp/gh-aw/mcp-logs/ 2>&1 | grep -v "^\\[" | head -1', + { + allowDomains: ['github.com'], + logLevel: 'debug', + timeout: 60000, + } + ); + + // With tmpfs mounted over the directory, ls should succeed but show empty directory + // The directory appears to exist (as an empty tmpfs) but contains no files + const allOutput = `${result.stdout}\n${result.stderr}`; + // Verify either: + // 1. Directory listing shows it's effectively empty (total size indicates empty tmpfs) + // 2. Or old /dev/null behavior ("Not a directory") + expect(allOutput).toMatch(/total|Not a directory|cannot access/i); + }, 120000); + + test('Test 14: /tmp/gh-aw/mcp-logs/ is hidden in chroot mode', async () => { + // Try to access the mcp-logs directory at /host path + const result = await runner.runWithSudo( + 'ls -la /host/tmp/gh-aw/mcp-logs/ 2>&1 | grep -v "^\\[" | head -1', + { + allowDomains: ['github.com'], + logLevel: 'debug', + timeout: 60000, + enableChroot: true, + } + ); + + // With tmpfs mounted over the directory at /host path, ls should succeed but show empty + const allOutput = `${result.stdout}\n${result.stderr}`; + expect(allOutput).toMatch(/total|Not a directory|cannot access/i); + }, 120000); + + test('Test 15: MCP logs files cannot be read in normal mode', async () => { + // Try to read a typical MCP log file path + const result = await runner.runWithSudo( + 'cat /tmp/gh-aw/mcp-logs/safeoutputs/log.txt 2>&1 | grep -v "^\\[" | head -1', + { + allowDomains: ['github.com'], + logLevel: 'debug', + timeout: 60000, + } + ); + + // Should fail with "No such file or directory" (tmpfs is empty) + // This confirms the tmpfs mount is preventing file access to host files + const allOutput = `${result.stdout}\n${result.stderr}`; + expect(allOutput).toMatch(/No such file or directory|Not a directory|cannot access/i); + }, 120000); + }); });