test(config): cover secret bundle inode races

This commit is contained in:
2026-07-12 11:09:09 +02:00
parent 390cfd24b5
commit 3807c65a41
2 changed files with 48 additions and 10 deletions
+31 -9
View File
@@ -34,6 +34,23 @@ export interface SecretBundleConfig {
THT_SECRETS_FILE?: string;
}
/** Injectable filesystem boundary used by the race-condition tests. */
export interface SecretBundleFsOps {
lstat(path: string): Stats;
open(path: string, flags: number): number;
fstat(fd: number): Stats;
read(fd: number): string;
close(fd: number): void;
}
const realFs: SecretBundleFsOps = {
lstat: lstatSync,
open: openSync,
fstat: fstatSync,
read: (fd) => readFileSync(fd, "utf8"),
close: closeSync,
};
function unavailable(): Error { return new Error("secret bundle is unavailable"); }
function secureStat(info: Stats, docker: boolean): boolean {
@@ -43,26 +60,26 @@ function secureStat(info: Stats, docker: boolean): boolean {
return info.uid === (process.getuid?.() ?? info.uid) && (mode === 0o400 || mode === 0o600);
}
function readSecure(file: string): string {
function readSecure(file: string, fs: SecretBundleFsOps): string {
let fd: number | undefined;
try {
if (!file || file.trim() !== file || file.includes("\0")) throw unavailable();
const docker = file.startsWith("/run/secrets/") && !file.slice("/run/secrets/".length).includes("/");
if (file.startsWith("/run/secrets/") && !docker) throw unavailable();
if (docker) {
const parent = lstatSync("/run/secrets");
const parent = fs.lstat("/run/secrets");
if (!parent.isDirectory() || parent.uid !== 0 || (parent.mode & 0o022) !== 0) throw unavailable();
}
const before = lstatSync(file);
const before = fs.lstat(file);
if (!secureStat(before, docker)) throw unavailable();
fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
const opened = fstatSync(fd);
fd = fs.open(file, constants.O_RDONLY | constants.O_NOFOLLOW);
const opened = fs.fstat(fd);
if (!secureStat(opened, docker) || before.dev !== opened.dev || before.ino !== opened.ino) throw unavailable();
return readFileSync(fd, "utf8");
return fs.read(fd);
} catch {
throw unavailable();
} finally {
if (fd !== undefined) try { closeSync(fd); } catch { /* sanitized by design */ }
if (fd !== undefined) try { fs.close(fd); } catch { /* sanitized by design */ }
}
}
@@ -86,7 +103,12 @@ function parseBundle(text: string): ReadonlyMap<string, string> {
}
export function loadSecretBundle(file: string): ReadonlyMap<string, string> {
try { return parseBundle(readSecure(file)); } catch { throw unavailable(); }
return loadSecretBundleWithFs(file, realFs);
}
/** Same loader with an injectable filesystem boundary; useful for TOCTOU tests. */
export function loadSecretBundleWithFs(file: string, fs: SecretBundleFsOps): ReadonlyMap<string, string> {
try { return parseBundle(readSecure(file, fs)); } catch { throw unavailable(); }
}
/** Resolve a value from the bundle, with the pre-bundle *_SECRET_FILE fallback. */
@@ -104,7 +126,7 @@ export function secretValue(config: SecretBundleConfig, key: string): string | u
})()
: undefined;
if (!legacyPath) return undefined;
const value = readSecure(legacyPath);
const value = readSecure(legacyPath, realFs);
if (!value || /\s/.test(value)) throw unavailable();
return value;
}