UNPKG

@juspay/neurolink

Version:

The pipe layer of an AI nervous system: one interface connecting provider neurons to your application, across three inference types — generate, stream and decide. `decide` returns typed, calibrated judgements from a non-generative model (~400ms, ~$0.00002

154 lines (153 loc) • 5.86 kB
/** * Path containment guard for the sandboxed execution paths: the built-in file * tools, bash's `cwd` argument and the background-command runner all check * against caller-declared roots here, so the strong form lives in one place. * * The load-bearing detail is **realpath**. A string comparison on resolved * paths is defeated by a symlink: `<root>/escape → /etc` resolves lexically to * `<root>/escape`, which passes, and then the command runs in `/etc`. Both * sides are therefore resolved through the filesystem before they are * compared, and a target that cannot be resolved is refused rather than * assumed innocent. * * The `+ sep` suffix on the prefix test is the other one: without it * `/home/app-evil` passes a containment check against `/home/app`. * * @module utils/pathSandbox */ import { lstatSync, realpathSync, statSync } from "node:fs"; import { basename, isAbsolute, join, resolve, sep } from "node:path"; /** Real path of an existing directory, or undefined. */ function realDirectory(candidate) { try { const real = realpathSync(candidate); return statSync(real).isDirectory() ? real : undefined; } catch { return undefined; } } /** Real path of anything that exists — file, directory or otherwise. */ function realEntry(candidate) { try { return realpathSync(candidate); } catch { return undefined; } } /** A symbolic link that does not resolve (dangling, or a loop). */ function isUnresolvableLink(candidate) { try { return lstatSync(candidate).isSymbolicLink(); } catch { return false; } } /** * Separator-bounded containment. A root that already ends in the separator * (`/`, `C:\\`) contains every path that starts with it; appending another * separator would turn `/` into `//` and deny everything. */ export function isPathInsideRoot(candidate, root) { if (candidate === root) { return true; } const prefix = root.endsWith(sep) ? root : root + sep; return candidate.startsWith(prefix); } const isInside = isPathInsideRoot; /** * Resolve `target` and confirm it is `root` or lies inside it, with symlinks * followed on both sides. * * Both must exist and be directories — a containment check on a path that is * not there yet cannot be honest, because whatever appears later may be a * symlink out. * * @param target Directory to check. Relative paths resolve against `root`. * @param root Sandbox root the target must not escape. * @returns The resolved REAL path, or the reason it was refused. */ export function resolveWithinRoot(target, root) { const realRoot = realDirectory(resolve(root)); if (!realRoot) { return { error: `Sandbox root "${root}" is not an existing directory. Point the policy's ` + "cwdRoot at a directory that exists before starting commands.", }; } const requested = isAbsolute(target) ? target : resolve(realRoot, target); const realTarget = realDirectory(requested); if (!realTarget) { return { error: `Working directory "${target}" is not an existing directory. Name a directory ` + `that exists inside ${realRoot}.`, }; } if (!isInside(realTarget, realRoot)) { return { error: `Access denied: "${target}" resolves to ${realTarget}, which is outside the ` + `permitted root ${realRoot}. Run the command inside that root instead.`, }; } return { path: realTarget }; } /** * The file twin of {@link resolveWithinRoot}: contain a path that may name a * file, or may not exist at all (a path argument to `git log`, say). * * A path that exists is resolved through its symlinks and checked directly. A * path that does not exist is checked on its nearest EXISTING ancestor, which * is the deepest point a symlink could redirect — everything below that is a * name, not a link. Deleted files therefore stay askable about, and * `../../etc/passwd` still does not. * * @param target Path to check. Relative paths resolve against `root`. * @param root Sandbox root the target must not escape. * @returns The resolved absolute path, or the reason it was refused. */ export function resolvePathWithinRoot(target, root) { const realRoot = realDirectory(resolve(root)); if (!realRoot) { return { error: `Sandbox root "${root}" is not an existing directory.` }; } const denied = { error: `Access denied: "${target}" resolves outside the permitted root ${realRoot}. ` + "Name a path inside it, relative to the root.", }; const requested = isAbsolute(target) ? resolve(target) : resolve(realRoot, target); const direct = realEntry(requested); if (direct) { return isInside(direct, realRoot) ? { path: direct } : denied; } // The path does not resolve. A link that exists but points nowhere is where // a write would be redirected — a dangling `<root>/x → /elsewhere/new` would // pass an ancestor check and then create `/elsewhere/new` — so any // unresolvable link on the way down is refused outright. if (isUnresolvableLink(requested)) { return denied; } let existing = resolve(requested, ".."); let suffix = basename(requested); for (;;) { const real = realDirectory(existing); if (real) { return isInside(real, realRoot) ? { path: resolve(real, suffix) } : denied; } if (isUnresolvableLink(existing)) { return denied; } const parent = resolve(existing, ".."); if (parent === existing) { return denied; } suffix = join(basename(existing), suffix); existing = parent; } }