Files
gbrain/test/serve-http-bootstrap-token.test.ts
T
bb3376e3b0 fix(security): prevent leaking admin bootstrap token to non-TTY stdout (#2625)
* fix(security): #2624 don't print admin bootstrap token on non-TTY (log-leak)

serve --http printed the generated admin token in the startup banner
unconditionally. In containerized deploys stderr ships to centralized log
storage, turning the token into a standing secret in logs.

Fail-safe default: the generated token now prints only when stderr is an
interactive TTY. Non-TTY starts hide it (--print-admin-token forces it;
$GBRAIN_ADMIN_BOOTSTRAP_TOKEN + --suppress-bootstrap-token already existed).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(security): #2624 banner shows 'from env' before non-TTY hidden guard

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-16 13:34:28 -07:00

99 lines
3.8 KiB
TypeScript

/**
* v0.36.1.x #1024: bootstrap token resolution rules.
*
* The env override (GBRAIN_ADMIN_BOOTSTRAP_TOKEN) is security-sensitive —
* a weak token silently accepted would let any reader of the env scope
* impersonate admin. Test the validation directly via the pure helper so
* the rule can't drift without the suite catching it.
*/
import { describe, test, expect } from 'bun:test';
import { resolveBootstrapToken, shouldSuppressBootstrapPrint } from '../src/commands/serve-http.ts';
describe('resolveBootstrapToken (v0.36.1.x #1024)', () => {
test('unset env → generates a fresh token via the injected RNG', () => {
const r = resolveBootstrapToken(undefined, () => 'a'.repeat(64));
expect(r.kind).toBe('ok');
if (r.kind === 'ok') {
expect(r.token).toBe('a'.repeat(64));
expect(r.fromEnv).toBe(false);
}
});
test('valid env token (32 chars, A-Za-z0-9_-) → ok + fromEnv:true', () => {
const r = resolveBootstrapToken('abcdef0123456789abcdef0123456789');
expect(r.kind).toBe('ok');
if (r.kind === 'ok') {
expect(r.token).toBe('abcdef0123456789abcdef0123456789');
expect(r.fromEnv).toBe(true);
}
});
test('valid env token with hyphens and underscores → accepted', () => {
const r = resolveBootstrapToken('A-B_C-D_E-F_G-H_I-J_K-L_M-N_O-P_');
expect(r.kind).toBe('ok');
});
test('token shorter than 32 chars → error, refuse to start', () => {
const r = resolveBootstrapToken('short-token-only-25-chars'); // 25 chars
expect(r.kind).toBe('error');
if (r.kind === 'error') {
expect(r.message).toMatch(/at least 32 chars/);
expect(r.message).toMatch(/Refusing to start/);
}
});
test('token with invalid characters (space, special chars) → error', () => {
const r1 = resolveBootstrapToken('contains space '.padEnd(40, 'x'));
expect(r1.kind).toBe('error');
const r2 = resolveBootstrapToken('contains$dollar'.padEnd(40, 'x'));
expect(r2.kind).toBe('error');
const r3 = resolveBootstrapToken('contains/slash'.padEnd(40, 'x'));
expect(r3.kind).toBe('error');
});
test('whitespace-padded token gets trimmed before validation', () => {
const r = resolveBootstrapToken(' abcdef0123456789abcdef0123456789 ');
expect(r.kind).toBe('ok');
if (r.kind === 'ok') expect(r.token).toBe('abcdef0123456789abcdef0123456789');
});
test('empty string env (after trim, zero chars) → error', () => {
const r = resolveBootstrapToken('');
expect(r.kind).toBe('error');
});
test('exactly 32 chars hex (the boundary) → accepted', () => {
const r = resolveBootstrapToken('0123456789abcdef0123456789abcdef');
expect(r.kind).toBe('ok');
});
test('31 chars (one short) → error', () => {
const r = resolveBootstrapToken('0123456789abcdef0123456789abcde'); // 31
expect(r.kind).toBe('error');
});
});
describe('shouldSuppressBootstrapPrint (#2624 log-leak default)', () => {
const base = { suppress: false, fromEnv: false, forcePrint: false, isTty: true };
test('generated token on non-TTY (container) → hidden by default', () => {
expect(shouldSuppressBootstrapPrint({ ...base, isTty: false })).toBe(true);
});
test('generated token on interactive TTY → printed', () => {
expect(shouldSuppressBootstrapPrint({ ...base, isTty: true })).toBe(false);
});
test('--print-admin-token forces raw value on non-TTY', () => {
expect(shouldSuppressBootstrapPrint({ ...base, isTty: false, forcePrint: true })).toBe(false);
});
test('env-sourced token is never printed', () => {
expect(shouldSuppressBootstrapPrint({ ...base, fromEnv: true, isTty: true })).toBe(true);
});
test('--suppress overrides even a forced print', () => {
expect(shouldSuppressBootstrapPrint({ ...base, suppress: true, forcePrint: true, isTty: true })).toBe(true);
});
});