test: add TOCTOU error path coverage for ssl-bump.ts#1161
test: add TOCTOU error path coverage for ssl-bump.ts#1161
Conversation
Add tests for: - EEXIST error handling (silently caught when files already exist) - Non-EEXIST errors re-thrown (EACCES on index.txt and size file) - Non-Error throw handling in generateSessionCa (string rejection) Fixes #837 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
✅ Coverage Check PassedOverall Coverage
📁 Per-file Coverage Changes (2 files)
Coverage comparison generated by |
There was a problem hiding this comment.
Pull request overview
Adds targeted tests to improve branch coverage for TOCTOU-related error handling paths in ssl-bump.ts, focusing on initSslDb filesystem errors and generateSessionCa non-Error rejections.
Changes:
- Added coverage for
generateSessionCahandling of non-Error(string) rejections. - Added coverage for
initSslDbbehavior onEEXIST(should be swallowed) vs non-EEXISTerrors (should re-throw). - Added permission-based tests to simulate
EACCESduring DB initialization.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| // Create ssl_db directory, then make it read-only so writeFileSync fails with EACCES | ||
| const sslDbPath = path.join(tempDir, 'ssl_db'); | ||
| fs.mkdirSync(path.join(sslDbPath, 'certs'), { recursive: true }); | ||
| // Make ssl_db read-only so index.txt creation fails with EACCES | ||
| fs.chmodSync(sslDbPath, 0o555); | ||
|
|
||
| try { | ||
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); | ||
| } finally { | ||
| // Restore permissions for cleanup | ||
| fs.chmodSync(sslDbPath, 0o700); | ||
| } | ||
| }); | ||
|
|
||
| it('should re-throw non-EEXIST errors from size file writeFileSync', async () => { | ||
| // Create full structure with index.txt, then make dir read-only | ||
| // so only the size file creation fails | ||
| const sslDbPath = path.join(tempDir, 'ssl_db'); | ||
| fs.mkdirSync(path.join(sslDbPath, 'certs'), { recursive: true }); | ||
| // Pre-create index.txt so it hits EEXIST (silently caught), then | ||
| // size file creation will fail because dir is read-only | ||
| fs.writeFileSync(path.join(sslDbPath, 'index.txt'), ''); | ||
| fs.chmodSync(sslDbPath, 0o555); | ||
|
|
||
| try { | ||
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); | ||
| } finally { | ||
| fs.chmodSync(sslDbPath, 0o700); |
There was a problem hiding this comment.
These tests rely on chmodSync causing EACCES, which is not portable (notably unreliable on Windows, and can behave differently under elevated/CI permissions). This can make the suite fail or become flaky across environments. Prefer mocking fs.writeFileSync to throw an error with code: 'EACCES' for the specific call(s), or conditionally skip these tests on platforms where POSIX permissions don’t apply.
| // Create ssl_db directory, then make it read-only so writeFileSync fails with EACCES | |
| const sslDbPath = path.join(tempDir, 'ssl_db'); | |
| fs.mkdirSync(path.join(sslDbPath, 'certs'), { recursive: true }); | |
| // Make ssl_db read-only so index.txt creation fails with EACCES | |
| fs.chmodSync(sslDbPath, 0o555); | |
| try { | |
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); | |
| } finally { | |
| // Restore permissions for cleanup | |
| fs.chmodSync(sslDbPath, 0o700); | |
| } | |
| }); | |
| it('should re-throw non-EEXIST errors from size file writeFileSync', async () => { | |
| // Create full structure with index.txt, then make dir read-only | |
| // so only the size file creation fails | |
| const sslDbPath = path.join(tempDir, 'ssl_db'); | |
| fs.mkdirSync(path.join(sslDbPath, 'certs'), { recursive: true }); | |
| // Pre-create index.txt so it hits EEXIST (silently caught), then | |
| // size file creation will fail because dir is read-only | |
| fs.writeFileSync(path.join(sslDbPath, 'index.txt'), ''); | |
| fs.chmodSync(sslDbPath, 0o555); | |
| try { | |
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); | |
| } finally { | |
| fs.chmodSync(sslDbPath, 0o700); | |
| // Create ssl_db directory | |
| const sslDbPath = path.join(tempDir, 'ssl_db'); | |
| fs.mkdirSync(path.join(sslDbPath, 'certs'), { recursive: true }); | |
| // Mock writeFileSync so that creating index.txt fails with EACCES | |
| const originalWriteFileSync = fs.writeFileSync; | |
| const writeSpy = jest | |
| .spyOn(fs, 'writeFileSync') | |
| .mockImplementation((file: any, data: any, options?: any): any => { | |
| if (typeof file === 'string' && file === path.join(sslDbPath, 'index.txt')) { | |
| const err: any = new Error('EACCES'); | |
| err.code = 'EACCES'; | |
| throw err; | |
| } | |
| return (originalWriteFileSync as any)(file, data, options); | |
| }); | |
| try { | |
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); | |
| } finally { | |
| writeSpy.mockRestore(); | |
| } | |
| }); | |
| it('should re-throw non-EEXIST errors from size file writeFileSync', async () => { | |
| // Create full structure with index.txt so it hits EEXIST (silently caught), | |
| // then simulate a failure only for the size file creation | |
| const sslDbPath = path.join(tempDir, 'ssl_db'); | |
| fs.mkdirSync(path.join(sslDbPath, 'certs'), { recursive: true }); | |
| // Pre-create index.txt | |
| fs.writeFileSync(path.join(sslDbPath, 'index.txt'), ''); | |
| // Mock writeFileSync so that creating the size file fails with EACCES | |
| const originalWriteFileSync = fs.writeFileSync; | |
| const writeSpy = jest | |
| .spyOn(fs, 'writeFileSync') | |
| .mockImplementation((file: any, data: any, options?: any): any => { | |
| if (typeof file === 'string' && file === path.join(sslDbPath, 'size')) { | |
| const err: any = new Error('EACCES'); | |
| err.code = 'EACCES'; | |
| throw err; | |
| } | |
| return (originalWriteFileSync as any)(file, data, options); | |
| }); | |
| try { | |
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); | |
| } finally { | |
| writeSpy.mockRestore(); |
| fs.chmodSync(sslDbPath, 0o555); | ||
|
|
||
| try { | ||
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); |
There was a problem hiding this comment.
Asserting via toThrow(/EACCES/) depends on the error message text, which can vary by OS/runtime. A more stable assertion is to check the error shape (e.g., code: 'EACCES') using await expect(...).rejects.toMatchObject({ code: 'EACCES' }) (or whatever the implementation throws), which reduces cross-platform brittleness.
| fs.chmodSync(sslDbPath, 0o555); | ||
|
|
||
| try { | ||
| await expect(initSslDb(tempDir)).rejects.toThrow(/EACCES/); |
There was a problem hiding this comment.
Asserting via toThrow(/EACCES/) depends on the error message text, which can vary by OS/runtime. A more stable assertion is to check the error shape (e.g., code: 'EACCES') using await expect(...).rejects.toMatchObject({ code: 'EACCES' }) (or whatever the implementation throws), which reduces cross-platform brittleness.
| }); | ||
|
|
||
| await expect(generateSessionCa({ workDir: tempDir })).rejects.toThrow( | ||
| 'Failed to generate SSL Bump CA: unexpected string rejection' |
There was a problem hiding this comment.
The assertion matches the full error string exactly, which is brittle if the implementation changes wording/punctuation while still preserving the intended behavior. Consider asserting on key substrings (e.g., regex containing both Failed to generate SSL Bump CA and unexpected string rejection) to keep the test focused on behavior rather than exact formatting.
| 'Failed to generate SSL Bump CA: unexpected string rejection' | |
| /Failed to generate SSL Bump CA[\s\S]*unexpected string rejection/ |
| expect(fs.readFileSync(path.join(sslDbPath, 'size'), 'utf-8')).toBe('42\n'); | ||
| }); | ||
|
|
||
| it('should re-throw non-EEXIST errors from writeFileSync', async () => { |
There was a problem hiding this comment.
The two non-EEXIST tests duplicate setup/teardown patterns (create structure → chmod → expect reject → restore). Consider extracting a small helper (e.g., withReadOnlyDir(sslDbPath, fn)) to reduce repetition and make the intent of each case (which write fails) stand out.
| } | ||
| }); | ||
|
|
||
| it('should re-throw non-EEXIST errors from size file writeFileSync', async () => { |
There was a problem hiding this comment.
The two non-EEXIST tests duplicate setup/teardown patterns (create structure → chmod → expect reject → restore). Consider extracting a small helper (e.g., withReadOnlyDir(sslDbPath, fn)) to reduce repetition and make the intent of each case (which write fails) stand out.
🧪 Build Test: Bun
Overall: ✅ PASS Bun v1.3.10
|
|
Smoke test results:
|
Go Build Test Results
Overall: ✅ PASS
|
Smoke Test Results✅ GitHub MCP: #1159 fix(security): eliminate TOCTOU race conditions in ssl-bump.ts | #1158 fix(security): stop logging partial token values Overall: PASS
|
.NET Build Test Results
Overall: PASS ✅ Run outputhello-world:
|
Build Test: Deno ✅
Overall: PASS
|
C++ Build Test Results
Overall: PASS ✅
|
🦀 Rust Build Test Results
Overall: ✅ PASS
|
|
Smoke test results for run 22734171796 —
Overall: PASS
|
Node.js Build Test Results ✅
Overall: PASS
|
Java Build Test Results
Overall: PASS ✅
|
Chroot Version Comparison Results
Result: ❌ Not all runtimes match — Python and Node.js versions differ between host and chroot environments.
|
Summary
initSslDb(silently caught when files already exist via'wx'flag)initSslDb(EACCES on index.txt and size file - should re-throw)generateSessionCa(string rejection path)These tests cover the error branches introduced by the TOCTOU fix in #1159, improving
ssl-bump.tsbranch coverage.Fixes #837
Test plan
npm test)npm run build)npm run lint)🤖 Generated with Claude Code