test: pin the security-property regression guards from pre-landing review

The pre-landing review found the fixes were correct but three regression guards
were missing — each pins a property whose silent revert would keep behavior
identical while reopening the hole:
- validateAuth: a static tripwire asserting crypto.timingSafeEqual + the
  got.length===want.length gate + the null-header guard (a revert to `===`
  keeps accept/reject green but restores the timing side-channel).
- redact: a table-driven loop over the exported URL_PASSWORD_PLACEHOLDER_WORDS
  so a typo or dropped entry can't silently start blocking a doc placeholder;
  plus a substring-can't-rescue-a-real-secret assertion.
- config: assert the self-contained .gitignore is written even when git already
  ignores .gstack/, proving the write precedes the isIgnoredByGit early return.
- bun-polyfill: cover the 128+signal exit branch (POSIX only).

URL_PASSWORD_PLACEHOLDER_WORDS is exported so the table test can't drift.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Garry Tan
2026-08-16 11:07:55 -07:00
co-authored by Claude Fable 5
parent 341d7be27c
commit 5d74ed7231
5 changed files with 67 additions and 1 deletions
+16
View File
@@ -64,6 +64,22 @@ describe('Server auth security', () => {
expect(scopeBlock).toContain('Domain not allowed');
});
// Test 1d: validateAuth compares the bearer token in CONSTANT TIME with a
// length gate. A revert to `header === \`Bearer ${authToken}\`` keeps
// accept/reject behavior identical (functional tests still pass) but silently
// reintroduces the byte-by-byte timing side-channel; dropping the length gate
// makes timingSafeEqual throw RangeError (500 instead of 401) on a wrong-length
// token. Pin both properties, mirroring the token-registry sibling guard.
test('validateAuth uses constant-time comparison with a length gate', () => {
const authBlock = sliceBetween(SERVER_SRC, 'function validateAuth(req: Request): boolean {', '// Factory-scoped shutdown');
expect(authBlock).toContain('crypto.timingSafeEqual');
expect(authBlock).toContain('got.length === want.length');
// The null-header guard must remain (Buffer.from(null) would otherwise throw).
expect(authBlock).toContain('header === null');
// The raw === comparison of the header against the bearer string must be gone.
expect(authBlock).not.toContain('header === `Bearer ${authToken}`');
});
// Test 2: /refs endpoint requires auth via validateAuth
test('/refs endpoint requires authentication', () => {
const refsBlock = sliceBetween(SERVER_SRC, "url.pathname === '/refs'", "url.pathname === '/activity/stream'");