42 lines
1.9 KiB
JavaScript
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
|