fix(auth): close diagnostic filesystem races

This commit is contained in:
2026-08-17 13:04:15 +02:00
parent be1724890a
commit 6d438f4c7e
16 changed files with 545 additions and 90 deletions
+43 -18
View File
@@ -1,5 +1,5 @@
import { spawn, spawnSync } from "node:child_process";
import { win32 } from "node:path";
import { posix, win32 } from "node:path";
import type { Readable, Writable } from "node:stream";
import { z } from "zod";
@@ -36,6 +36,7 @@ export interface WindowsAuthStoragePage {
/** Internal adapter boundary for the file-session store's native Windows path. */
export interface WindowsAuthStorageBridge {
validateRoot(root: string): Promise<void>;
ensureLayout(root: string): Promise<void>;
readAuthConfig(path: string): Buffer;
create(root: string, directory: WindowsAuthStorageDirectory, filename: string, contents: Buffer): Promise<boolean>;
read(root: string, directory: WindowsAuthStorageDirectory, filename: string): Promise<Buffer | undefined>;
@@ -100,6 +101,8 @@ export interface WindowsAuthStorageBridgeOptions {
beforeInputForTest?: () => Promise<void>;
}
type AuthStoragePathStyle = "posix" | "windows";
const responseSchema = z.strictObject({
version: z.literal(PROTOCOL_VERSION),
ok: z.literal(true),
@@ -114,13 +117,14 @@ const responseSchema = z.strictObject({
})).max(MAX_ENTRIES).optional(),
more: z.boolean().optional(),
validated: z.boolean().optional(),
prepared: z.boolean().optional(),
});
type BridgeResponse = z.infer<typeof responseSchema>;
interface BridgeRequest {
version: typeof PROTOCOL_VERSION;
operation: "validate-root" | "read-auth-config" | "create" | "read" | "replace" | "remove" | "list" | "claim-consume" | "read-claim" | "remove-claim";
operation: "validate-root" | "ensure-layout" | "read-auth-config" | "create" | "read" | "replace" | "remove" | "list" | "claim-consume" | "read-claim" | "remove-claim";
root: string;
directory?: WindowsAuthStorageDirectory;
filename?: string;
@@ -145,9 +149,10 @@ function canonicalBase64(value: string, maximum: number): Buffer {
}
}
function validateRoot(root: string): void {
function validateRoot(root: string, pathStyle: AuthStoragePathStyle): void {
const paths = pathStyle === "windows" ? win32 : posix;
if (typeof root !== "string" || root.length === 0 || /[\u0000-\u001f\u007f]/.test(root)
|| !win32.isAbsolute(root) || win32.normalize(root) !== root) throw invalid();
|| !paths.isAbsolute(root) || paths.normalize(root) !== root) throw invalid();
}
function validateFilename(filename: string, allowClaim = false, allowOidcSlot = false): void {
@@ -156,11 +161,13 @@ function validateFilename(filename: string, allowClaim = false, allowOidcSlot =
&& !(allowOidcSlot && OIDC_SLOT_FILENAME.test(filename)))) throw invalid();
}
function safeThtExecutable(value: string | undefined): string {
function safeThtExecutable(value: string | undefined, pathStyle: AuthStoragePathStyle): string {
const executable = value ?? process.env.THT_BIN ?? "tht";
if (typeof executable !== "string" || executable.length === 0 || /[\u0000-\u001f\u007f]/.test(executable)) throw invalid();
if (executable === "tht" || executable === "tht.exe") return executable;
if (win32.isAbsolute(executable) && win32.normalize(executable) === executable && /\.exe$/i.test(executable)) return executable;
if (executable === "tht" || (pathStyle === "windows" && executable === "tht.exe")) return executable;
const paths = pathStyle === "windows" ? win32 : posix;
if (paths.isAbsolute(executable) && paths.normalize(executable) === executable
&& (pathStyle === "posix" || /\.exe$/i.test(executable))) return executable;
throw invalid();
}
@@ -178,9 +185,9 @@ function parseResponse(result: WindowsAuthStorageInvocationResult, maximumOutput
}
}
function encodedRequest(request: BridgeRequest): Buffer {
validateRoot(request.root);
if (request.operation === "validate-root") {
function encodedRequest(request: BridgeRequest, pathStyle: AuthStoragePathStyle): Buffer {
validateRoot(request.root, pathStyle);
if (request.operation === "validate-root" || request.operation === "ensure-layout") {
if (request.directory !== undefined || request.filename !== undefined || request.contentBase64 !== undefined
|| request.maximumEntries !== undefined || request.afterName !== undefined || request.continuation !== undefined) throw invalid();
} else if (request.operation === "read-auth-config") {
@@ -390,8 +397,11 @@ function listedEntries(
return response.entries.map((entry) => ({ name: entry.name, modifiedUnixMs: entry.modifiedUnixMs }));
}
export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge {
const executable = safeThtExecutable(options.thtExecutable);
function createAuthStorageBridge(
pathStyle: AuthStoragePathStyle,
options: WindowsAuthStorageBridgeOptions = {},
): WindowsAuthStorageBridge {
const executable = safeThtExecutable(options.thtExecutable, pathStyle);
const invoke = options.invoke ?? ((invocation: WindowsAuthStorageInvocation) => invokeTht(
invocation,
options.spawnChild,
@@ -404,7 +414,7 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
const response = await invoke({
executable,
args: ["_auth-storage"],
input: encodedRequest(value),
input: encodedRequest(value, pathStyle),
timeoutMs: TIMEOUT_MS,
maximumOutputBytes,
});
@@ -418,7 +428,7 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
const response = invokeSync({
executable,
args: ["_auth-storage"],
input: encodedRequest(value),
input: encodedRequest(value, pathStyle),
timeoutMs: TIMEOUT_MS,
maximumOutputBytes: MAX_AUTH_CONFIG_RESPONSE_BYTES,
});
@@ -442,12 +452,18 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
if (response.validated !== true
|| Object.keys(response).some((key) => !["version", "ok", "validated"].includes(key))) throw invalid();
},
async ensureLayout(root) {
const response = await request({ version: PROTOCOL_VERSION, operation: "ensure-layout", root });
if (response.prepared !== true
|| Object.keys(response).some((key) => !["version", "ok", "prepared"].includes(key))) throw invalid();
},
readAuthConfig(path) {
const paths = pathStyle === "windows" ? win32 : posix;
if (typeof path !== "string" || path.length === 0 || /[\u0000-\u001f\u007f]/.test(path)
|| !win32.isAbsolute(path) || win32.normalize(path) !== path) throw invalid();
const root = win32.dirname(path);
const filename = win32.basename(path);
if (!AUTH_CONFIG_FILENAME.test(filename) || win32.join(root, filename) !== path) throw invalid();
|| !paths.isAbsolute(path) || paths.normalize(path) !== path) throw invalid();
const root = paths.dirname(path);
const filename = paths.basename(path);
if (!AUTH_CONFIG_FILENAME.test(filename) || paths.join(root, filename) !== path) throw invalid();
const response = syncRequest({ version: PROTOCOL_VERSION, operation: "read-auth-config", root, filename });
if (Object.keys(response).some((key) => !["version", "ok", "found", "contentBase64"].includes(key))) throw invalid();
const contents = contentFrom(response, MAX_AUTH_CONFIG_BYTES);
@@ -520,3 +536,12 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
},
};
}
export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge {
return createAuthStorageBridge("windows", options);
}
/** POSIX uses the same single hidden tht protocol and bounds, with native canonical path rules. */
export function createPosixAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge {
return createAuthStorageBridge("posix", options);
}