From e10ad83d59f3f487420980d38cad8091141cb75d Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 11 Mar 2026 00:54:13 +0000 Subject: [PATCH 1/5] test: expand credential hiding tests to cover all 14 protected paths Add 3 new integration tests covering all 11 untested credential paths: SSH keys (4), AWS creds/config, Kube config, Azure creds, GCloud creds, Cargo creds, Composer auth. Tests verify 0 bytes at both direct home and /host chroot paths. Uses robust patterns (if -f, || true, extractCommandOutput) consistent with existing tests. Fixes #761 Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/credential-hiding.test.ts | 89 +++++++++++++++++++++ 1 file changed, 89 insertions(+) diff --git a/tests/integration/credential-hiding.test.ts b/tests/integration/credential-hiding.test.ts index 6354e0710..75a6b38fb 100644 --- a/tests/integration/credential-hiding.test.ts +++ b/tests/integration/credential-hiding.test.ts @@ -227,6 +227,95 @@ describe('Credential Hiding Security', () => { }, 120000); }); + describe('All 14 Credential Paths Coverage', () => { + // These tests cover the 11 credential paths not tested by Tests 1-4 above. + // Each path is hidden via /dev/null mount and should return empty content. + + const untestedPaths = [ + { name: 'SSH id_rsa', path: '.ssh/id_rsa' }, + { name: 'SSH id_ed25519', path: '.ssh/id_ed25519' }, + { name: 'SSH id_ecdsa', path: '.ssh/id_ecdsa' }, + { name: 'SSH id_dsa', path: '.ssh/id_dsa' }, + { name: 'AWS credentials', path: '.aws/credentials' }, + { name: 'AWS config', path: '.aws/config' }, + { name: 'Kube config', path: '.kube/config' }, + { name: 'Azure credentials', path: '.azure/credentials' }, + { name: 'GCloud credentials.db', path: '.config/gcloud/credentials.db' }, + { name: 'Cargo credentials', path: '.cargo/credentials' }, + { name: 'Composer auth.json', path: '.composer/auth.json' }, + ]; + + test('All untested credential files are hidden at direct home path (0 bytes)', async () => { + const homeDir = os.homedir(); + const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' '); + + // Check all credential files in a single container run for efficiency. + // wc -c reports byte count; /dev/null-mounted files should be 0 bytes. + // Use '|| true' to prevent failures when files don't exist + const result = await runner.runWithSudo( + `sh -c 'for f in ${paths}; do if [ -f "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, + { + allowDomains: ['github.com'], + logLevel: 'debug', + timeout: 60000, + } + ); + + expect(result).toSucceed(); + const cleanOutput = extractCommandOutput(result.stdout); + const lines = cleanOutput.split('\n').filter(l => l.match(/^\s*\d+/)); + // Each file should be 0 bytes (hidden via /dev/null) + lines.forEach(line => { + const size = parseInt(line.trim().split(/\s+/)[0]); + expect(size).toBe(0); + }); + // Verify we checked all 11 files + expect(lines.length).toBe(untestedPaths.length); + }, 120000); + + test('All untested credential files are hidden at /host path (0 bytes)', async () => { + const homeDir = os.homedir(); + const paths = untestedPaths.map(p => `/host${homeDir}/${p.path}`).join(' '); + + const result = await runner.runWithSudo( + `sh -c 'for f in ${paths}; do if [ -f "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, + { + allowDomains: ['github.com'], + logLevel: 'debug', + timeout: 60000, + } + ); + + expect(result).toSucceed(); + const cleanOutput = extractCommandOutput(result.stdout); + const lines = cleanOutput.split('\n').filter(l => l.match(/^\s*\d+/)); + lines.forEach(line => { + const size = parseInt(line.trim().split(/\s+/)[0]); + expect(size).toBe(0); + }); + expect(lines.length).toBe(untestedPaths.length); + }, 120000); + + test('cat on each untested credential file returns empty content', async () => { + const homeDir = os.homedir(); + const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' '); + + // cat all files and concatenate output - should be empty + const result = await runner.runWithSudo( + `sh -c 'for f in ${paths}; do if [ -f "$f" ]; then cat "$f"; fi; done 2>&1 || true'`, + { + allowDomains: ['github.com'], + logLevel: 'debug', + timeout: 60000, + } + ); + + expect(result).toSucceed(); + // All content should be empty (no credential data leaked) + const cleanOutput = extractCommandOutput(result.stdout).trim(); + expect(cleanOutput).toBe(''); + }, 120000); + }); describe('Security Verification', () => { test('Test 12: Simulated exfiltration attack gets empty data', async () => { From df5a3afb09702a342fa7051e328dfe3b5bc09114 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 11 Mar 2026 01:20:32 +0000 Subject: [PATCH 2/5] fix(test): create dummy credential files before testing /dev/null mounts On CI runners, credential files like ~/.ssh/id_rsa don't exist, so AWF skips the /dev/null mount and the tests find 0 files instead of 11. Create dummy files in beforeAll and clean up in afterAll. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/credential-hiding.test.ts | 34 +++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/integration/credential-hiding.test.ts b/tests/integration/credential-hiding.test.ts index 75a6b38fb..161b51424 100644 --- a/tests/integration/credential-hiding.test.ts +++ b/tests/integration/credential-hiding.test.ts @@ -245,6 +245,40 @@ describe('Credential Hiding Security', () => { { name: 'Composer auth.json', path: '.composer/auth.json' }, ]; + // Track files we create so we only clean up what we added + const createdFiles: string[] = []; + const createdDirs: string[] = []; + + beforeAll(() => { + // Create dummy credential files on the host so AWF will mount /dev/null over them. + // Without these files existing, AWF skips the /dev/null mount and the files + // simply don't exist inside the container. + const homeDir = os.homedir(); + for (const p of untestedPaths) { + const fullPath = `${homeDir}/${p.path}`; + if (!fs.existsSync(fullPath)) { + const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); + if (!fs.existsSync(dir)) { + fs.mkdirSync(dir, { recursive: true }); + createdDirs.push(dir); + } + fs.writeFileSync(fullPath, 'DUMMY_SECRET_VALUE'); + createdFiles.push(fullPath); + } + } + }); + + afterAll(() => { + // Clean up only the files/dirs we created + for (const f of createdFiles) { + try { fs.unlinkSync(f); } catch { /* ignore */ } + } + // Remove dirs in reverse order (deepest first) + for (const d of createdDirs.reverse()) { + try { fs.rmdirSync(d); } catch { /* ignore if not empty */ } + } + }); + test('All untested credential files are hidden at direct home path (0 bytes)', async () => { const homeDir = os.homedir(); const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' '); From 15c1f4aa271930913a543ab2326773445e1ce6a2 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 11 Mar 2026 01:33:34 +0000 Subject: [PATCH 3/5] fix(test): use atomic wx flag to avoid TOCTOU race in credential setup Replace existsSync+writeFileSync with writeFileSync({flag:'wx'}) to eliminate the file-system-race CodeQL alert in the test beforeAll hook. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/credential-hiding.test.ts | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/integration/credential-hiding.test.ts b/tests/integration/credential-hiding.test.ts index 161b51424..902c61c86 100644 --- a/tests/integration/credential-hiding.test.ts +++ b/tests/integration/credential-hiding.test.ts @@ -256,14 +256,20 @@ describe('Credential Hiding Security', () => { const homeDir = os.homedir(); for (const p of untestedPaths) { const fullPath = `${homeDir}/${p.path}`; - if (!fs.existsSync(fullPath)) { - const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir, { recursive: true }); - createdDirs.push(dir); - } - fs.writeFileSync(fullPath, 'DUMMY_SECRET_VALUE'); + const dir = fullPath.substring(0, fullPath.lastIndexOf('/')); + fs.mkdirSync(dir, { recursive: true }); + if (!createdDirs.includes(dir)) { + createdDirs.push(dir); + } + try { + // Use 'wx' flag: atomic create-if-not-exists (avoids TOCTOU race) + fs.writeFileSync(fullPath, 'DUMMY_SECRET_VALUE', { flag: 'wx' }); createdFiles.push(fullPath); + } catch (err: unknown) { + // EEXIST means file already exists, which is fine + if (err instanceof Error && 'code' in err && (err as NodeJS.ErrnoException).code !== 'EEXIST') { + throw err; + } } } }); From 03ad6ac6f6e954c9efa581176ea277bad6ed9542 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 11 Mar 2026 17:29:40 +0000 Subject: [PATCH 4/5] fix(test): use [ -e ] instead of [ -f ] for /dev/null-mounted files /dev/null-mounted credential files are character special devices, not regular files. [ -f ] returns false for them, causing wc -c to produce no output. Use [ -e ] (exists) to correctly detect these mounts. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/credential-hiding.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/tests/integration/credential-hiding.test.ts b/tests/integration/credential-hiding.test.ts index 902c61c86..470474c82 100644 --- a/tests/integration/credential-hiding.test.ts +++ b/tests/integration/credential-hiding.test.ts @@ -292,8 +292,10 @@ describe('Credential Hiding Security', () => { // Check all credential files in a single container run for efficiency. // wc -c reports byte count; /dev/null-mounted files should be 0 bytes. // Use '|| true' to prevent failures when files don't exist + // Use [ -e ] instead of [ -f ] because /dev/null-mounted files are + // character special devices, not regular files const result = await runner.runWithSudo( - `sh -c 'for f in ${paths}; do if [ -f "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, + `sh -c 'for f in ${paths}; do if [ -e "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, { allowDomains: ['github.com'], logLevel: 'debug', @@ -317,8 +319,10 @@ describe('Credential Hiding Security', () => { const homeDir = os.homedir(); const paths = untestedPaths.map(p => `/host${homeDir}/${p.path}`).join(' '); + // Use [ -e ] instead of [ -f ] because /dev/null-mounted files are + // character special devices, not regular files const result = await runner.runWithSudo( - `sh -c 'for f in ${paths}; do if [ -f "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, + `sh -c 'for f in ${paths}; do if [ -e "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, { allowDomains: ['github.com'], logLevel: 'debug', @@ -341,8 +345,10 @@ describe('Credential Hiding Security', () => { const paths = untestedPaths.map(p => `${homeDir}/${p.path}`).join(' '); // cat all files and concatenate output - should be empty + // Use [ -e ] instead of [ -f ] because /dev/null-mounted files are + // character special devices, not regular files const result = await runner.runWithSudo( - `sh -c 'for f in ${paths}; do if [ -f "$f" ]; then cat "$f"; fi; done 2>&1 || true'`, + `sh -c 'for f in ${paths}; do if [ -e "$f" ]; then cat "$f"; fi; done 2>&1 || true'`, { allowDomains: ['github.com'], logLevel: 'debug', From 3cfdcf8955e7a830f1d4cbdc6bc9bad81f257e48 Mon Sep 17 00:00:00 2001 From: "Jiaxiao (mossaka) Zhou" Date: Wed, 11 Mar 2026 17:56:49 +0000 Subject: [PATCH 5/5] fix(test): verify /host paths are inaccessible in chroot mode AWF always runs in chroot mode (chroot /host), so /host$HOME/... paths don't exist inside the container. Changed the test from expecting 0-byte files at /host paths to verifying those paths are inaccessible, which is the correct security assertion for chroot mode. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/integration/credential-hiding.test.ts | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/tests/integration/credential-hiding.test.ts b/tests/integration/credential-hiding.test.ts index 470474c82..3aae59cd9 100644 --- a/tests/integration/credential-hiding.test.ts +++ b/tests/integration/credential-hiding.test.ts @@ -315,14 +315,15 @@ describe('Credential Hiding Security', () => { expect(lines.length).toBe(untestedPaths.length); }, 120000); - test('All untested credential files are hidden at /host path (0 bytes)', async () => { + test('All untested credential files are inaccessible at /host path (chroot prevents access)', async () => { const homeDir = os.homedir(); const paths = untestedPaths.map(p => `/host${homeDir}/${p.path}`).join(' '); - // Use [ -e ] instead of [ -f ] because /dev/null-mounted files are - // character special devices, not regular files + // AWF always runs in chroot mode (chroot /host), so /host$HOME/... paths + // don't exist inside the container — they're already inside the chroot. + // This verifies that credentials can't be exfiltrated via /host prefix paths. const result = await runner.runWithSudo( - `sh -c 'for f in ${paths}; do if [ -e "$f" ]; then wc -c "$f"; fi; done 2>&1 || true'`, + `sh -c 'count=0; for f in ${paths}; do if [ -e "$f" ]; then count=$((count+1)); fi; done; echo "accessible: $count"'`, { allowDomains: ['github.com'], logLevel: 'debug', @@ -332,12 +333,8 @@ describe('Credential Hiding Security', () => { expect(result).toSucceed(); const cleanOutput = extractCommandOutput(result.stdout); - const lines = cleanOutput.split('\n').filter(l => l.match(/^\s*\d+/)); - lines.forEach(line => { - const size = parseInt(line.trim().split(/\s+/)[0]); - expect(size).toBe(0); - }); - expect(lines.length).toBe(untestedPaths.length); + // No files should be accessible at /host paths inside chroot + expect(cleanOutput).toContain('accessible: 0'); }, 120000); test('cat on each untested credential file returns empty content', async () => {