fix(auth): make diagnostics bounded and portable
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { createHash, hkdfSync, randomBytes } from "node:crypto";
|
||||
import {
|
||||
chmodSync,
|
||||
accessSync,
|
||||
closeSync,
|
||||
constants,
|
||||
fchmodSync,
|
||||
@@ -297,6 +297,10 @@ function isNotFound(error: unknown): boolean {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "ENOENT";
|
||||
}
|
||||
|
||||
function isAlreadyExists(error: unknown): boolean {
|
||||
return (error as NodeJS.ErrnoException | undefined)?.code === "EEXIST";
|
||||
}
|
||||
|
||||
function canonicalRawValue(value: string): boolean {
|
||||
if (typeof value !== "string" || !TOKEN_PATTERN.test(value)) return false;
|
||||
try {
|
||||
@@ -377,26 +381,12 @@ function directoryScanIdentity(path: string): DirectoryScanIdentity {
|
||||
return { ...identity, mtimeMs: info.mtimeMs, ctimeMs: info.ctimeMs };
|
||||
}
|
||||
|
||||
function privateDirectory(path: string): void {
|
||||
let created = false;
|
||||
try {
|
||||
lstatSync(path);
|
||||
} catch (error) {
|
||||
if (!isNotFound(error)) throw invalid();
|
||||
try {
|
||||
mkdirSync(path, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
||||
chmodSync(path, PRIVATE_DIRECTORY_MODE);
|
||||
created = true;
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
}
|
||||
try {
|
||||
directoryIdentity(path);
|
||||
} catch {
|
||||
if (!created) throw invalid();
|
||||
throw invalid();
|
||||
}
|
||||
interface SessionRootAncestor {
|
||||
path: string;
|
||||
descriptor: number;
|
||||
dev: number;
|
||||
ino: number;
|
||||
uid: number;
|
||||
}
|
||||
|
||||
function validateSessionRootSyntax(root: string): void {
|
||||
@@ -404,11 +394,90 @@ function validateSessionRootSyntax(root: string): void {
|
||||
|| root.includes("\0") || /\p{Cc}/u.test(root) || !isAbsolute(root) || normalize(root) !== root) throw invalid();
|
||||
}
|
||||
|
||||
function sameAncestor(ancestor: SessionRootAncestor, info: Stats): boolean {
|
||||
return info.isDirectory() && !info.isSymbolicLink() && ancestor.dev === info.dev
|
||||
&& ancestor.ino === info.ino && ancestor.uid === info.uid;
|
||||
}
|
||||
|
||||
function withSessionRootPreflight<T>(root: string, use: (exists: boolean) => T): T {
|
||||
validateSessionRootSyntax(root);
|
||||
const ancestors: SessionRootAncestor[] = [];
|
||||
try {
|
||||
const components = root.split("/").filter((component) => component.length > 0);
|
||||
let current = "/";
|
||||
for (let index = -1; index < components.length; index += 1) {
|
||||
if (index >= 0) current = join(current, components[index]!);
|
||||
let info: Stats;
|
||||
try {
|
||||
info = lstatSync(current) as Stats;
|
||||
} catch (error) {
|
||||
if (!isNotFound(error) || index !== components.length - 1) throw invalid();
|
||||
accessSync(dirname(current), constants.W_OK | constants.X_OK);
|
||||
for (const ancestor of ancestors) {
|
||||
const observed = lstatSync(ancestor.path) as Stats;
|
||||
if (!sameAncestor(ancestor, observed) || realpathSync(ancestor.path) !== ancestor.path) throw invalid();
|
||||
}
|
||||
const result = use(false);
|
||||
for (const ancestor of ancestors) {
|
||||
const observed = lstatSync(ancestor.path) as Stats;
|
||||
if (!sameAncestor(ancestor, observed) || realpathSync(ancestor.path) !== ancestor.path) throw invalid();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
if (!info.isDirectory() || info.isSymbolicLink() || realpathSync(current) !== current) throw invalid();
|
||||
const descriptor = openSync(current, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0)
|
||||
| (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
||||
const opened = fstatSync(descriptor) as Stats;
|
||||
if (!opened.isDirectory() || opened.dev !== info.dev || opened.ino !== info.ino || opened.uid !== info.uid) {
|
||||
closeSync(descriptor);
|
||||
throw invalid();
|
||||
}
|
||||
ancestors.push({ path: current, descriptor, dev: info.dev, ino: info.ino, uid: info.uid });
|
||||
}
|
||||
directoryIdentity(root);
|
||||
const result = use(true);
|
||||
for (const ancestor of ancestors) {
|
||||
const observed = lstatSync(ancestor.path) as Stats;
|
||||
if (!sameAncestor(ancestor, observed) || realpathSync(ancestor.path) !== ancestor.path) throw invalid();
|
||||
}
|
||||
return result;
|
||||
} catch {
|
||||
throw invalid();
|
||||
} finally {
|
||||
for (const ancestor of ancestors.reverse()) {
|
||||
try { closeSync(ancestor.descriptor); } catch { /* preflight has already failed closed */ }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function privateDirectory(path: string): void {
|
||||
withSessionRootPreflight(path, (exists) => {
|
||||
if (exists) return;
|
||||
try {
|
||||
mkdirSync(path, { recursive: false, mode: PRIVATE_DIRECTORY_MODE });
|
||||
} catch (error) {
|
||||
if (!isAlreadyExists(error)) throw invalid();
|
||||
directoryIdentity(path);
|
||||
return;
|
||||
}
|
||||
const descriptor = openSync(path, constants.O_RDONLY | (constants.O_DIRECTORY ?? 0)
|
||||
| (constants.O_NOFOLLOW ?? 0) | (constants.O_NONBLOCK ?? 0));
|
||||
try {
|
||||
fchmodSync(descriptor, PRIVATE_DIRECTORY_MODE);
|
||||
const opened = fstatSync(descriptor) as Stats;
|
||||
const current = directoryIdentity(path);
|
||||
if (opened.dev !== current.dev || opened.ino !== current.ino || opened.uid !== current.uid
|
||||
|| (opened.mode & 0o7777) !== current.mode) throw invalid();
|
||||
} finally {
|
||||
try { closeSync(descriptor); } catch { /* creation already fails closed */ }
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/** Side-effect-free POSIX validator shared by runtime storage and static diagnostics. */
|
||||
export function validateAuthSessionRoot(root: string): void {
|
||||
try {
|
||||
validateSessionRootSyntax(root);
|
||||
directoryIdentity(root);
|
||||
withSessionRootPreflight(root, () => undefined);
|
||||
} catch {
|
||||
throw invalid();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user