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:
+19
-4
@@ -67,9 +67,21 @@ initAuditLog(config.auditLog);
|
||||
// ─── Auth ───────────────────────────────────────────────────────
|
||||
// AUTH_TOKEN is injectable via process.env.AUTH_TOKEN so embedders
|
||||
// (gbrowser's gbd daemon spawn) can pre-allocate the token and hand it to
|
||||
// the Bun child via env. Whitespace-only values fall back to randomUUID so
|
||||
// the security boundary is never silently weakened by misconfiguration.
|
||||
const AUTH_TOKEN = (process.env.AUTH_TOKEN?.trim()) || crypto.randomUUID();
|
||||
// the Bun child via env.
|
||||
//
|
||||
// Validation: require >= 16 chars after stripping ALL unicode whitespace
|
||||
// (not just ASCII — .trim() misses U+200B / U+FEFF / U+00A0 / etc., which
|
||||
// would otherwise let a misconfigured embedder ship a one-character BOM as
|
||||
// the bearer secret). Reject tokens that are too short or contain only
|
||||
// whitespace; fall back to randomUUID so the security boundary is never
|
||||
// silently weakened by misconfiguration.
|
||||
function sanitizeAuthToken(raw: string | undefined): string | null {
|
||||
if (!raw) return null;
|
||||
const stripped = raw.replace(/[\s -]/g, '');
|
||||
if (stripped.length < 16) return null;
|
||||
return stripped;
|
||||
}
|
||||
const AUTH_TOKEN = sanitizeAuthToken(process.env.AUTH_TOKEN) || crypto.randomUUID();
|
||||
initRegistry(AUTH_TOKEN);
|
||||
const BROWSE_PORT = parseInt(process.env.BROWSE_PORT || '0', 10);
|
||||
const IDLE_TIMEOUT_MS = parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10); // 30 min
|
||||
@@ -178,7 +190,10 @@ export function resolveConfigFromEnv(): Omit<ServerConfig, 'browserManager' | 's
|
||||
config: ReturnType<typeof resolveConfig>;
|
||||
} {
|
||||
return {
|
||||
authToken: (process.env.AUTH_TOKEN?.trim()) || crypto.randomUUID(),
|
||||
// Same sanitizer as the module-level AUTH_TOKEN: strips ALL unicode
|
||||
// whitespace and rejects tokens shorter than 16 chars so a misconfigured
|
||||
// embedder can't ship a BOM/zero-width as the bearer secret.
|
||||
authToken: sanitizeAuthToken(process.env.AUTH_TOKEN) || crypto.randomUUID(),
|
||||
browsePort: parseInt(process.env.BROWSE_PORT || '0', 10),
|
||||
idleTimeoutMs: parseInt(process.env.BROWSE_IDLE_TIMEOUT || '1800000', 10),
|
||||
config: resolveConfig(),
|
||||
|
||||
Reference in New Issue
Block a user