53 lines
1.8 KiB
TypeScript
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');
|
|
}
|
|
}
|