-
Notifications
You must be signed in to change notification settings - Fork 19
test: expand credential hiding tests to all 14 protected paths #1163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
e10ad83
test: expand credential hiding tests to cover all 14 protected paths
Mossaka df5a3af
fix(test): create dummy credential files before testing /dev/null mounts
Mossaka 15c1f4a
fix(test): use atomic wx flag to avoid TOCTOU race in credential setup
Mossaka 03ad6ac
fix(test): use [ -e ] instead of [ -f ] for /dev/null-mounted files
Mossaka 3cfdcf8
fix(test): verify /host paths are inaccessible in chroot mode
Mossaka File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -227,6 +227,138 @@ 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' }, | ||
| ]; | ||
|
|
||
| // 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}`; | ||
| 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; | ||
| } | ||
| } | ||
| } | ||
| }); | ||
|
|
||
| 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(' '); | ||
|
|
||
| // 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 [ -e "$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 inaccessible at /host path (chroot prevents access)', async () => { | ||
| const homeDir = os.homedir(); | ||
| const paths = untestedPaths.map(p => `/host${homeDir}/${p.path}`).join(' '); | ||
|
|
||
| // 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 'count=0; for f in ${paths}; do if [ -e "$f" ]; then count=$((count+1)); fi; done; echo "accessible: $count"'`, | ||
| { | ||
| allowDomains: ['github.com'], | ||
| logLevel: 'debug', | ||
| timeout: 60000, | ||
| } | ||
| ); | ||
|
|
||
| expect(result).toSucceed(); | ||
| const cleanOutput = extractCommandOutput(result.stdout); | ||
| // 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 () => { | ||
| const homeDir = os.homedir(); | ||
| 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 [ -e "$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 () => { | ||
|
|
||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Building a shell command by interpolating
pathsdirectly intosh -c '...'is unsafe and can break whenhomeDircontains spaces or shell-special characters (and is also an injection risk since this runs with sudo). Prefer passing file paths as positional args tosh -cand iterating over"$@"(or otherwise robustly quoting/escaping each path) so the loop receives the exact paths regardless of characters.