Files
2026-07-31 13:12:54 -04:00

42 lines
1.9 KiB
JavaScript

import path from 'node:path';
import { config } from '../config.js';
export class SandboxViolationError extends Error {
constructor(message) {
super(message);
this.name = 'SandboxViolationError';
}
}
/**
* Resolves `relativePath` against `root` and guarantees the result stays
* inside `root`. Throws SandboxViolationError on any attempt to escape
* (e.g. via `..`, absolute paths outside the root, or symlink-style tricks
* handled at the OS level by the caller when it actually touches the fs).
*/
export function resolveWorkspacePath(root, relativePath) {
const absoluteRoot = path.resolve(root);
const target = path.resolve(absoluteRoot, relativePath || '.');
const normalizedRoot = absoluteRoot.toLowerCase();
const normalizedTarget = target.toLowerCase();
if (normalizedTarget !== normalizedRoot && !normalizedTarget.startsWith(normalizedRoot + path.sep)) {
throw new SandboxViolationError(`Refusing to access "${relativePath}": it resolves outside the workspace root (${absoluteRoot}).`);
}
return target;
}
function basenameNoExt(binary) {
const base = path.basename(binary);
return base.replace(/\.(exe|cmd|bat|ps1)$/i, '');
}
/**
* Throws unless `binary` is on the configured allow-list. Command
* construction elsewhere in the codebase always passes `args` as a separate
* array (never a single shell string), so this allow-list is the only line
* of defense needed against arbitrary command execution.
*/
export function assertAllowedBinary(binary) {
const name = basenameNoExt(binary).toLowerCase();
const allowed = config.allowedBinaries.some((candidate) => candidate.toLowerCase() === name);
if (!allowed) {
throw new SandboxViolationError(`Refusing to run "${binary}": it is not on the allow-list (${config.allowedBinaries.join(', ')}).`);
}
}
//# sourceMappingURL=sandbox.js.map