mirror of
https://github.com/garrytan/gstack.git
synced 2026-06-23 18:20:00 +02:00
fix(token-registry): UTF-8 byte-length short-circuit before timingSafeEqual
Constant-time compare on the root token now compares UTF-8 byte lengths before crypto.timingSafeEqual, which throws on length-mismatched buffers. A multibyte input whose JS string length matches but byte length differs no longer crashes on the auth path; isRootToken returns false instead. Tests cover the four interesting cases: multibyte byte-length mismatch, extra-prefix length mismatch, same-length last-byte flip, and empty input against a set root. Contributed by @RagavRida (#1416). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,7 +155,20 @@ export function getRootToken(): string {
|
||||
}
|
||||
|
||||
export function isRootToken(token: string): boolean {
|
||||
return token === rootToken;
|
||||
// Constant-time compare so a tunnel-reachable caller who can provoke an
|
||||
// isRootToken() call (e.g., via the 403 "root over tunnel" rejection path)
|
||||
// can't measure byte-by-byte string-compare timing to recover the token.
|
||||
// Compare UTF-8 byte lengths (not JS string length) before timingSafeEqual,
|
||||
// which throws on length-mismatched buffers. A multibyte input whose JS
|
||||
// string length matches rootToken but whose UTF-8 byte length differs must
|
||||
// return false on the auth path, not error out.
|
||||
if (!rootToken) return false;
|
||||
const tokenBytes = Buffer.byteLength(token, 'utf8');
|
||||
const rootBytes = Buffer.byteLength(rootToken, 'utf8');
|
||||
if (tokenBytes !== rootBytes) return false;
|
||||
const a = Buffer.from(token, 'utf8');
|
||||
const b = Buffer.from(rootToken, 'utf8');
|
||||
return crypto.timingSafeEqual(a, b);
|
||||
}
|
||||
|
||||
function generateToken(prefix: string): string {
|
||||
|
||||
Reference in New Issue
Block a user