fix(backend): harden principal child isolation

This commit is contained in:
User
2026-07-16 18:38:40 +02:00
parent 458eb13c89
commit b454fb478b
9 changed files with 167 additions and 26 deletions
+24 -2
View File
@@ -1,4 +1,4 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { chmodSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { homedir } from "node:os";
import { join } from "node:path";
import { randomUUID } from "node:crypto";
@@ -10,6 +10,25 @@ export interface PrincipalContext {
isAdmin: boolean;
}
const principalEnvKeys = [
"THT_PRINCIPAL_ISSUER", "THT_PRINCIPAL_SUBJECT", "THT_PRINCIPAL_DISPLAY_NAME", "THT_PRINCIPAL_IS_ADMIN",
] as const;
export function clearPrincipalEnvironment(env: NodeJS.ProcessEnv): void {
for (const key of principalEnvKeys) delete env[key];
}
export function expandLocalHome(path: string, home = homedir()): string {
if (path === "~") return home;
if (path.startsWith("~/")) return join(home, path.slice(2));
return path;
}
function harden(path: string, mode: number): void {
if (process.platform === "win32") return;
try { chmodSync(path, mode); } catch { /* best-effort parity with harness local storage */ }
}
const invalid = (value: string) => value.length === 0 || value.length > 512 || /[\u0000-\u001f\u007f]/.test(value);
function required(value: unknown): string | undefined {
@@ -34,12 +53,14 @@ export function upstreamPrincipal(headers: Record<string, unknown>): PrincipalCo
}
export function localPrincipal(): PrincipalContext {
const home = process.env.THT_HOME ?? join(homedir(), ".thothii");
const home = expandLocalHome(process.env.THT_HOME ?? join(homedir(), ".thothii"));
const identityPath = join(home, "identity.json");
mkdirSync(home, { recursive: true, mode: 0o700 });
harden(home, 0o700);
try {
const stored = JSON.parse(readFileSync(identityPath, "utf8"));
if (stored?.issuer === "local" && typeof stored.subject === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i.test(stored.subject)) {
harden(identityPath, 0o600);
return { issuer: "local", subject: stored.subject, isAdmin: false };
}
throw new Error("invalid local identity");
@@ -48,6 +69,7 @@ export function localPrincipal(): PrincipalContext {
const principal = { issuer: "local", subject: randomUUID() };
try {
writeFileSync(identityPath, JSON.stringify(principal) + "\n", { mode: 0o600, flag: "wx" });
harden(identityPath, 0o600);
return { ...principal, isAdmin: false };
} catch (writeError: any) {
// Another local request won the identity creation race; always converge on its UUID.