Files
stack/packages/mosaic/src/commands/brain-secure-config.ts
T

53 lines
1.8 KiB
TypeScript

import { lstatSync } from 'node:fs';
import { dirname, relative, resolve, sep } from 'node:path';
import { assertCanonicalContainment, readRegularFileSecure } from '../fleet/secure-file.js';
const MAX_CONFIG_BYTES = 256 * 1024;
const GROUP_OR_OTHER_WRITE = 0o022;
function currentUid(): number {
if (typeof process.getuid !== 'function') {
throw new Error('config-owner-check-unsupported');
}
return process.getuid();
}
function assertOwnedNonWritableDirectory(path: string, uid: number): void {
const status = lstatSync(path);
if (!status.isDirectory() || status.isSymbolicLink() || status.uid !== uid) {
throw new Error('config-ancestor-owner-unsafe');
}
if ((status.mode & GROUP_OR_OTHER_WRITE) !== 0) {
throw new Error('config-ancestor-permissions-unsafe');
}
}
export function readBrainConfigSecure(path: string, root: string): string {
const canonicalRoot = resolve(root);
const canonicalPath = resolve(path);
assertCanonicalContainment(canonicalRoot, canonicalPath);
const uid = currentUid();
assertOwnedNonWritableDirectory(canonicalRoot, uid);
let cursor = canonicalRoot;
for (const component of relative(canonicalRoot, dirname(canonicalPath))
.split(sep)
.filter(Boolean)) {
cursor = resolve(cursor, component);
assertOwnedNonWritableDirectory(cursor, uid);
}
const snapshot = readRegularFileSecure(canonicalPath, {
root: canonicalRoot,
maxBytes: MAX_CONFIG_BYTES,
});
if (snapshot.uid !== uid) throw new Error('config-file-owner-unsafe');
if ((snapshot.mode & GROUP_OR_OTHER_WRITE) !== 0) {
throw new Error('config-file-permissions-unsafe');
}
try {
return new TextDecoder('utf-8', { fatal: true }).decode(snapshot.content);
} catch {
throw new Error('config-file-not-utf8');
}
}