fix(auth): paginate session maintenance safely
This commit is contained in:
@@ -24,6 +24,11 @@ export interface WindowsAuthStorageEntry {
|
||||
modifiedUnixMs: number;
|
||||
}
|
||||
|
||||
export interface WindowsAuthStoragePage {
|
||||
entries: WindowsAuthStorageEntry[];
|
||||
more: boolean;
|
||||
}
|
||||
|
||||
/** Internal adapter boundary for the file-session store's native Windows path. */
|
||||
export interface WindowsAuthStorageBridge {
|
||||
create(root: string, directory: WindowsAuthStorageDirectory, filename: string, contents: Buffer): Promise<boolean>;
|
||||
@@ -35,6 +40,12 @@ export interface WindowsAuthStorageBridge {
|
||||
directory: WindowsAuthStorageDirectory,
|
||||
maximumEntries?: number,
|
||||
): Promise<WindowsAuthStorageEntry[]>;
|
||||
listPage(
|
||||
root: string,
|
||||
directory: "sessions",
|
||||
afterName: string | undefined,
|
||||
maximumEntries: number,
|
||||
): Promise<WindowsAuthStoragePage>;
|
||||
claimConsume(root: string, filename: string): Promise<Buffer | undefined>;
|
||||
readClaim(root: string, filename: string): Promise<Buffer | undefined>;
|
||||
removeClaim(root: string, filename: string): Promise<boolean>;
|
||||
@@ -92,6 +103,7 @@ const responseSchema = z.strictObject({
|
||||
name: z.string().max(128),
|
||||
modifiedUnixMs: z.number().int().safe().nonnegative(),
|
||||
})).max(MAX_ENTRIES).optional(),
|
||||
more: z.boolean().optional(),
|
||||
});
|
||||
|
||||
type BridgeResponse = z.infer<typeof responseSchema>;
|
||||
@@ -104,6 +116,8 @@ interface BridgeRequest {
|
||||
filename?: string;
|
||||
contentBase64?: string;
|
||||
maximumEntries?: number;
|
||||
afterName?: string;
|
||||
continuation?: true;
|
||||
}
|
||||
|
||||
function directoryMaximum(directory: WindowsAuthStorageDirectory): number {
|
||||
@@ -159,8 +173,15 @@ function encodedRequest(request: BridgeRequest): Buffer {
|
||||
if (request.filename !== undefined || request.contentBase64 !== undefined) throw invalid();
|
||||
if (request.maximumEntries !== undefined && (!Number.isInteger(request.maximumEntries)
|
||||
|| request.maximumEntries < 1 || request.maximumEntries > MAX_ENTRIES)) throw invalid();
|
||||
if (request.continuation === true) {
|
||||
if (request.directory !== "sessions" || (request.afterName !== undefined && !DIGEST_FILENAME.test(request.afterName))) {
|
||||
throw invalid();
|
||||
}
|
||||
} else if (request.afterName !== undefined || request.continuation !== undefined) {
|
||||
throw invalid();
|
||||
}
|
||||
} else {
|
||||
if (request.maximumEntries !== undefined) throw invalid();
|
||||
if (request.maximumEntries !== undefined || request.afterName !== undefined || request.continuation !== undefined) throw invalid();
|
||||
if (request.filename === undefined) throw invalid();
|
||||
const allowClaim = request.operation === "remove" && request.directory === "oidc";
|
||||
const allowOidcSlot = request.directory === "oidc"
|
||||
@@ -311,6 +332,22 @@ function contentFrom(response: BridgeResponse, maximum: number): Buffer | undefi
|
||||
return canonicalBase64(response.contentBase64, maximum);
|
||||
}
|
||||
|
||||
function listedEntries(
|
||||
response: BridgeResponse,
|
||||
directory: WindowsAuthStorageDirectory,
|
||||
maximumEntries: number,
|
||||
): WindowsAuthStorageEntry[] {
|
||||
if (response.entries === undefined || response.entries.length > maximumEntries) throw invalid();
|
||||
const names = new Set<string>();
|
||||
for (const entry of response.entries) {
|
||||
if (!DIGEST_FILENAME.test(entry.name) && !(directory === "oidc"
|
||||
&& (CLAIM_FILENAME.test(entry.name) || OIDC_SLOT_FILENAME.test(entry.name)))) throw invalid();
|
||||
if (names.has(entry.name)) throw invalid();
|
||||
names.add(entry.name);
|
||||
}
|
||||
return response.entries.map((entry) => ({ name: entry.name, modifiedUnixMs: entry.modifiedUnixMs }));
|
||||
}
|
||||
|
||||
export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridgeOptions = {}): WindowsAuthStorageBridge {
|
||||
const executable = safeThtExecutable(options.thtExecutable);
|
||||
const invoke = options.invoke ?? ((invocation: WindowsAuthStorageInvocation) => invokeTht(
|
||||
@@ -368,13 +405,32 @@ export function createWindowsAuthStorageBridge(options: WindowsAuthStorageBridge
|
||||
directory,
|
||||
maximumEntries,
|
||||
});
|
||||
if (response.entries === undefined) throw invalid();
|
||||
if (response.entries.length > maximumEntries) throw invalid();
|
||||
for (const entry of response.entries) {
|
||||
if (!DIGEST_FILENAME.test(entry.name) && !(directory === "oidc"
|
||||
&& (CLAIM_FILENAME.test(entry.name) || OIDC_SLOT_FILENAME.test(entry.name)))) throw invalid();
|
||||
if (response.more !== undefined) throw invalid();
|
||||
return listedEntries(response, directory, maximumEntries);
|
||||
},
|
||||
async listPage(root, directory, afterName, maximumEntries) {
|
||||
if (directory !== "sessions" || !Number.isInteger(maximumEntries)
|
||||
|| maximumEntries < 1 || maximumEntries > MAX_ENTRIES
|
||||
|| (afterName !== undefined && !DIGEST_FILENAME.test(afterName))) throw invalid();
|
||||
const response = await request({
|
||||
version: PROTOCOL_VERSION,
|
||||
operation: "list",
|
||||
root,
|
||||
directory,
|
||||
maximumEntries,
|
||||
continuation: true,
|
||||
...(afterName === undefined ? {} : { afterName }),
|
||||
});
|
||||
if (response.more === undefined) throw invalid();
|
||||
const entries = listedEntries(response, directory, maximumEntries);
|
||||
let previous = afterName;
|
||||
for (const entry of entries) {
|
||||
if (previous !== undefined && entry.name <= previous) throw invalid();
|
||||
previous = entry.name;
|
||||
}
|
||||
return response.entries.map((entry) => ({ name: entry.name, modifiedUnixMs: entry.modifiedUnixMs }));
|
||||
if (response.more && entries.length !== maximumEntries) throw invalid();
|
||||
if (response.more && (previous === undefined || previous === afterName)) throw invalid();
|
||||
return { entries, more: response.more };
|
||||
},
|
||||
async claimConsume(root, filename) {
|
||||
return contentFrom(await request(recordRequest("claim-consume", root, "oidc", filename)), MAX_OIDC_BYTES);
|
||||
|
||||
Reference in New Issue
Block a user