fix(auth): close diagnostic filesystem races
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user