mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 10:12:11 +03:00
checkKnownPath() only validated the RESOLVED realpath target against EXPECTED_PARENT_PATHS, never crediting that the candidate path itself was already constructed from a trusted root by getKnownToolPaths(). Version managers that install via symlinks/junctions (nvm-windows and more broadly nvm/asdf/pyenv-style tools) place the shim inside a trusted root but its resolved target lives in a private per-version store outside the allowlist, so it was misreported as symlink_escape (#7753). Fix: trust a candidate location if EITHER its original path OR its resolved target falls within EXPECTED_PARENT_PATHS. Separately, locateCommandCandidate() short-circuited on the FIRST known-path candidate that returned any non-not_found failure reason, without trying the remaining candidates or ever falling back to a real PATH search. A single stray artifact at one guessed Windows install location for claude therefore poisoned detection entirely even when the real binary was resolvable via PATH (#7774). Fix: remember non-fatal known-path failures but keep walking every candidate, and always fall through to the PATH-based search before giving up. Extracted both helpers into a new cliRuntimeKnownPath.ts module (dependency injected, no circular import) to keep cliRuntime.ts under its frozen file-size budget.
80 lines
3.0 KiB
TypeScript
80 lines
3.0 KiB
TypeScript
/**
|
|
* Known-install-path trust & candidate-matching helpers for cliRuntime.ts.
|
|
*
|
|
* Extracted from cliRuntime.ts (which is at its frozen file-size budget) so the
|
|
* #7753/#7774 fixes don't grow that file. Both helpers are dependency-injected
|
|
* (no import back into cliRuntime.ts) to avoid a circular module reference.
|
|
*/
|
|
|
|
export type PathWithinFn = (childPath: string, parentPath: string) => boolean;
|
|
|
|
/**
|
|
* #7753 — nvm-windows/nvm/asdf/pyenv symlink-escape false positive.
|
|
*
|
|
* Check whether either the original (pre-resolution) candidate path or its
|
|
* resolved realpath target falls within a trusted parent directory. Checking
|
|
* both — not just the resolved target — credits that `commandPath` was already
|
|
* constructed from a trusted root by `getKnownToolPaths()`, so a symlink/junction
|
|
* placed there by a version manager (nvm-windows, nvm, asdf, pyenv) whose
|
|
* resolved target lives in a private per-version store outside
|
|
* `EXPECTED_PARENT_PATHS` is still recognized as legitimate. A symlink whose
|
|
* ORIGINAL location is also untrusted (not reachable through the trusted-root
|
|
* candidate generator) stays rejected.
|
|
*/
|
|
export const isLocationTrusted = async (
|
|
commandPath: string,
|
|
realPath: string,
|
|
expectedParentPaths: string[],
|
|
isPathWithin: PathWithinFn,
|
|
realpath: (targetPath: string) => Promise<string>
|
|
): Promise<boolean> => {
|
|
for (const parent of expectedParentPaths) {
|
|
if (isPathWithin(commandPath, parent) || isPathWithin(realPath, parent)) {
|
|
return true;
|
|
}
|
|
|
|
try {
|
|
const resolvedParent = await realpath(parent);
|
|
if (isPathWithin(commandPath, resolvedParent) || isPathWithin(realPath, resolvedParent)) {
|
|
return true;
|
|
}
|
|
} catch {
|
|
// Ignore missing/unresolvable parents and continue checking the remaining ones.
|
|
}
|
|
}
|
|
return false;
|
|
};
|
|
|
|
export type KnownPathCheckResult = {
|
|
installed: boolean;
|
|
commandPath: string | null;
|
|
reason: string | null;
|
|
};
|
|
|
|
/**
|
|
* #7774 — known-path short-circuit hides a genuinely runnable binary.
|
|
*
|
|
* Walk the known-install-path candidates for a tool. A genuine positive
|
|
* identification (installed, or installed-but-not-executable) returns
|
|
* immediately. Any other failure reason (unsafe_path/symlink_escape/
|
|
* suspicious_size/not_file/…) is only REMEMBERED, not returned — a single stray
|
|
* artifact at one guessed location must not stop the remaining candidates (or
|
|
* the PATH fallback in the caller) from being tried.
|
|
*/
|
|
export const findKnownPathMatch = async <T extends KnownPathCheckResult>(
|
|
knownPaths: string[],
|
|
checkKnownPath: (candidatePath: string) => Promise<T>
|
|
): Promise<{ match: T | null; bestFailure: T | null }> => {
|
|
let bestFailure: T | null = null;
|
|
for (const knownPath of knownPaths) {
|
|
const result = await checkKnownPath(knownPath);
|
|
if (result.installed) {
|
|
return { match: result, bestFailure: null };
|
|
}
|
|
if (result.reason && result.reason !== "not_found" && !bestFailure) {
|
|
bestFailure = result;
|
|
}
|
|
}
|
|
return { match: null, bestFailure };
|
|
};
|