mirror of
https://github.com/garrytan/gstack.git
synced 2026-07-05 15:47:57 +02:00
fix: harden auth-token validation, TDZ try/catch, lockfile path safety
Three security hardening fixes from /ship adversarial review: 1. AUTH_TOKEN unicode-whitespace bypass (server.ts:67-83). Old: `process.env.AUTH_TOKEN?.trim() || randomUUID()` only stripped ASCII whitespace. A misconfigured embedder shipping AUTH_TOKEN=$'' (BOM) or $'' (zero-width space) would silently get a one-character bearer secret. New `sanitizeAuthToken()` strips all unicode whitespace via regex and requires >= 16 chars after stripping; anything shorter falls back to crypto.randomUUID(). Same sanitizer used by `resolveConfigFromEnv()` so the embedder path is hardened too. 2. security-classifier.ts checkTranscript safety net. `resolveClaudeCommand()` and `spawn()` can throw under transient conditions (PATH probe failure, posix_spawn ENOMEM). Old code let the throw propagate and rejected the Promise with a raw exception. Now wrapped in try/catch that calls finish() with a degraded signal, matching the graceful-degradation contract the layer already promises for missing-CLI / exit-nonzero / parse-error. 3. cleanSingletonLocks defensive guard tightened (config.ts). Old: basename === 'chromium-profile' OR userDataDir === $CHROMIUM_PROFILE. The second branch was env-controlled and the first was bypassable by passing a relative path that resolved to chromium-profile via CWD drift. New guard: refuses relative paths outright, resolves both sides via path.resolve(), and only accepts the env-match path when $CHROMIUM_PROFILE is itself absolute. Test updates: replace the old `.trim()` test with three new cases covering unicode-whitespace stripping, short-token rejection, and zero-width-only rejection (server-factory.test.ts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -53,12 +53,41 @@ describe('server.ts factory API surface', () => {
|
||||
}
|
||||
});
|
||||
|
||||
test('AUTH_TOKEN value is trimmed', () => {
|
||||
test('AUTH_TOKEN whitespace is stripped (including unicode whitespace)', () => {
|
||||
const orig = process.env.AUTH_TOKEN;
|
||||
process.env.AUTH_TOKEN = ' padded-token ';
|
||||
// 22 chars after stripping leading/trailing whitespace including BOM (U+FEFF)
|
||||
// and zero-width space (U+200B), so passes the 16-char minimum.
|
||||
process.env.AUTH_TOKEN = ' padded-token-abc123xyz ';
|
||||
try {
|
||||
const cfg = resolveConfigFromEnv();
|
||||
expect(cfg.authToken).toBe('padded-token');
|
||||
expect(cfg.authToken).toBe('padded-token-abc123xyz');
|
||||
} finally {
|
||||
if (orig === undefined) delete process.env.AUTH_TOKEN;
|
||||
else process.env.AUTH_TOKEN = orig;
|
||||
}
|
||||
});
|
||||
|
||||
test('AUTH_TOKEN shorter than 16 chars after stripping falls back to randomUUID', () => {
|
||||
const orig = process.env.AUTH_TOKEN;
|
||||
// Only 5 chars of content — too short for the 16-char minimum.
|
||||
process.env.AUTH_TOKEN = 'short';
|
||||
try {
|
||||
const cfg = resolveConfigFromEnv();
|
||||
// Must be a UUID, not the rejected short token.
|
||||
expect(cfg.authToken).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
||||
} finally {
|
||||
if (orig === undefined) delete process.env.AUTH_TOKEN;
|
||||
else process.env.AUTH_TOKEN = orig;
|
||||
}
|
||||
});
|
||||
|
||||
test('AUTH_TOKEN of only zero-width unicode whitespace falls back to randomUUID', () => {
|
||||
const orig = process.env.AUTH_TOKEN;
|
||||
// U+200B (ZWSP), U+FEFF (BOM), U+00A0 (NBSP) — would pass .trim() but not the unicode-aware strip.
|
||||
process.env.AUTH_TOKEN = ' ';
|
||||
try {
|
||||
const cfg = resolveConfigFromEnv();
|
||||
expect(cfg.authToken).toMatch(/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/);
|
||||
} finally {
|
||||
if (orig === undefined) delete process.env.AUTH_TOKEN;
|
||||
else process.env.AUTH_TOKEN = orig;
|
||||
|
||||
Reference in New Issue
Block a user