fix(security): audit oauth lock hash

This commit is contained in:
Vincent Koc
2026-06-10 09:32:32 +09:00
parent 48ec58a584
commit 5967ae61bd
2 changed files with 9 additions and 10 deletions
@@ -66,11 +66,13 @@ describe("resolveOAuthRefreshLockPath", () => {
it("is immune to simple concat collisions at the provider/profile boundary", () => {
// With a plain `${provider}:${profileId}` hash input, the pair
// ("a", "b:c") would collide with ("a:b", "c"). The NUL separator
// in the hash input rules that out.
// ("a", "b:c") would collide with ("a:b", "c"). Tuple encoding rules that out.
expect(resolveOAuthRefreshLockPath("a", "b:c")).not.toBe(
resolveOAuthRefreshLockPath("a:b", "c"),
);
expect(resolveOAuthRefreshLockPath("a", "\x00b")).not.toBe(
resolveOAuthRefreshLockPath("a\x00", "b"),
);
});
it("keeps lock filenames short for long profile ids", () => {
+5 -8
View File
@@ -47,10 +47,10 @@ export function resolveAuthStatePathForDisplay(agentDir?: string): string {
/**
* Resolve the path of the cross-agent, per-profile OAuth refresh coordination
* lock. The filename hashes `provider\0profileId` so it is filesystem-safe
* lock. The filename hashes a JSON tuple of `[provider, profileId]` so it is filesystem-safe
* for arbitrary unicode/control-character inputs and always bounded in
* length. The NUL separator makes it impossible to collide two distinct
* `(provider, profileId)` pairs by string concatenation.
* length. Tuple encoding makes it impossible to collide two distinct
* `(provider, profileId)` pairs by separator-sensitive string concatenation.
*
* This lock is the serialization point that prevents the `refresh_token_reused`
* storm when N agents share one OAuth profile (see issue #26322): every agent
@@ -63,13 +63,10 @@ export function resolveAuthStatePathForDisplay(agentDir?: string): string {
* test fixture, etc.) do not needlessly serialize against each other.
*/
export function resolveOAuthRefreshLockPath(provider: string, profileId: string): string {
const hash = createHash("sha256");
const lockKey = JSON.stringify([provider, profileId]);
// This hashes provider/profile identifiers into a path-safe lock name; it is
// not password storage or credential verification.
hash.update(provider, "utf8");
hash.update("\u0000", "utf8"); // NUL separator: unambiguous boundary.
// codeql[js/insufficient-password-hash]
hash.update(profileId, "utf8");
const safeId = `sha256-${hash.digest("hex")}`;
const safeId = `sha256-${createHash("sha256").update(lockKey, "utf8").digest("hex")}`;
return path.join(resolveStateDir(), "locks", "oauth-refresh", safeId);
}