Files
openclaw/src/infra/windows-install-roots.ts
Anmol Ahuja c40884d306 Prefer non-user writeable paths (#54346)
* infra: trust system binary roots

* infra: isolate windows install root overrides

* infra: narrow windows reg lookup

* browser: restore windows executable comments

---------

Co-authored-by: Jacob Tomlinson <jtomlinson@nvidia.com>
2026-03-27 11:29:32 +00:00

264 lines
7.8 KiB
TypeScript

import { execFileSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
const DEFAULT_SYSTEM_ROOT = "C:\\Windows";
const DEFAULT_PROGRAM_FILES = "C:\\Program Files";
const DEFAULT_PROGRAM_FILES_X86 = "C:\\Program Files (x86)";
const WINDOWS_NT_CURRENT_VERSION_KEY = "HKLM\\SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion";
const WINDOWS_CURRENT_VERSION_KEY = "HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion";
const REG_QUERY_TIMEOUT_MS = 5_000;
type QueryRegistryValue = (key: string, valueName: string) => string | null;
type IsReadableFile = (filePath: string) => boolean;
type WindowsInstallRootsTestOverrides = {
queryRegistryValue?: QueryRegistryValue;
isReadableFile?: IsReadableFile;
};
export type WindowsInstallRoots = {
systemRoot: string;
programFiles: string;
programFilesX86: string;
programW6432: string | null;
};
let queryRegistryValueFn: QueryRegistryValue = defaultQueryRegistryValue;
let isReadableFileFn: IsReadableFile = defaultIsReadableFile;
let cachedProcessInstallRoots: WindowsInstallRoots | null = null;
function defaultIsReadableFile(filePath: string): boolean {
try {
fs.accessSync(filePath, fs.constants.R_OK);
return true;
} catch {
return false;
}
}
function trimTrailingSeparators(value: string): string {
const parsed = path.win32.parse(value);
let trimmed = value;
while (trimmed.length > parsed.root.length && /[\\/]/.test(trimmed.at(-1) ?? "")) {
trimmed = trimmed.slice(0, -1);
}
return trimmed;
}
/**
* Windows install roots should be local absolute directories, not drive-relative
* paths, UNC shares, or PATH-like lists that could widen trust unexpectedly.
*/
export function normalizeWindowsInstallRoot(raw: string | undefined): string | null {
if (typeof raw !== "string") {
return null;
}
const trimmed = raw.trim();
if (
!trimmed ||
trimmed.includes("\0") ||
trimmed.includes("\r") ||
trimmed.includes("\n") ||
trimmed.includes(";")
) {
return null;
}
const normalized = trimTrailingSeparators(path.win32.normalize(trimmed));
if (!path.win32.isAbsolute(normalized) || normalized.startsWith("\\\\")) {
return null;
}
const parsed = path.win32.parse(normalized);
if (!/^[A-Za-z]:\\$/.test(parsed.root)) {
return null;
}
if (normalized.length <= parsed.root.length) {
return null;
}
return normalized;
}
function getEnvValueCaseInsensitive(
env: Record<string, string | undefined>,
expectedKey: string,
): string | undefined {
const direct = env[expectedKey];
if (direct !== undefined) {
return direct;
}
const upper = expectedKey.toUpperCase();
const actualKey = Object.keys(env).find((key) => key.toUpperCase() === upper);
return actualKey ? env[actualKey] : undefined;
}
function getWindowsRegExeCandidates(env: Record<string, string | undefined>): readonly string[] {
const seen = new Set<string>();
const candidates: string[] = [];
for (const root of [
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(env, "SystemRoot")),
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(env, "WINDIR")),
DEFAULT_SYSTEM_ROOT,
]) {
if (!root) {
continue;
}
const key = root.toLowerCase();
if (seen.has(key)) {
continue;
}
seen.add(key);
candidates.push(path.win32.join(root, "System32", "reg.exe"));
}
return candidates;
}
function locateWindowsRegExe(env: Record<string, string | undefined> = process.env): string | null {
for (const candidate of getWindowsRegExeCandidates(env)) {
if (isReadableFileFn(candidate)) {
return candidate;
}
}
return null;
}
function escapeRegex(value: string): string {
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function parseRegQueryValue(stdout: string, valueName: string): string | null {
const pattern = new RegExp(`^\\s*${escapeRegex(valueName)}\\s+REG_[A-Z0-9_]+\\s+(.+)$`, "im");
const match = stdout.match(pattern);
return match?.[1]?.trim() || null;
}
function runRegQuery(
regExe: string,
key: string,
valueName: string,
use64BitView: boolean,
): string {
const args = ["query", key, "/v", valueName];
if (use64BitView) {
args.push("/reg:64");
}
return execFileSync(regExe, args, {
encoding: "utf8",
stdio: ["ignore", "pipe", "ignore"],
timeout: REG_QUERY_TIMEOUT_MS,
windowsHide: true,
});
}
function defaultQueryRegistryValue(key: string, valueName: string): string | null {
const regExe = locateWindowsRegExe(process.env);
if (!regExe) {
return null;
}
for (const use64BitView of [true, false]) {
try {
const stdout = runRegQuery(regExe, key, valueName, use64BitView);
const parsed = parseRegQueryValue(stdout, valueName);
if (parsed) {
return parsed;
}
} catch {
// Keep trying alternate registry views or fallbacks below.
}
}
return null;
}
function getRegistryInstallRoots(): Partial<WindowsInstallRoots> {
return {
systemRoot:
normalizeWindowsInstallRoot(
queryRegistryValueFn(WINDOWS_NT_CURRENT_VERSION_KEY, "SystemRoot") ?? undefined,
) ?? undefined,
programFiles:
normalizeWindowsInstallRoot(
queryRegistryValueFn(WINDOWS_CURRENT_VERSION_KEY, "ProgramFilesDir") ?? undefined,
) ?? undefined,
programFilesX86:
normalizeWindowsInstallRoot(
queryRegistryValueFn(WINDOWS_CURRENT_VERSION_KEY, "ProgramFilesDir (x86)") ?? undefined,
) ?? undefined,
programW6432:
normalizeWindowsInstallRoot(
queryRegistryValueFn(WINDOWS_CURRENT_VERSION_KEY, "ProgramW6432Dir") ?? undefined,
) ?? undefined,
};
}
function buildWindowsInstallRoots(
env: Record<string, string | undefined>,
useRegistryRoots: boolean,
): WindowsInstallRoots {
const registryRoots = useRegistryRoots ? getRegistryInstallRoots() : {};
const envProgramW6432 = normalizeWindowsInstallRoot(
getEnvValueCaseInsensitive(env, "ProgramW6432"),
);
const programW6432 = registryRoots.programW6432 ?? envProgramW6432 ?? null;
return {
systemRoot:
registryRoots.systemRoot ??
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(env, "SystemRoot")) ??
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(env, "WINDIR")) ??
DEFAULT_SYSTEM_ROOT,
programFiles:
registryRoots.programFiles ??
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(env, "ProgramFiles")) ??
programW6432 ??
DEFAULT_PROGRAM_FILES,
programFilesX86:
registryRoots.programFilesX86 ??
normalizeWindowsInstallRoot(getEnvValueCaseInsensitive(env, "ProgramFiles(x86)")) ??
DEFAULT_PROGRAM_FILES_X86,
programW6432,
};
}
export function getWindowsInstallRoots(
env: Record<string, string | undefined> = process.env,
): WindowsInstallRoots {
if (env === process.env) {
cachedProcessInstallRoots ??= buildWindowsInstallRoots(env, true);
return cachedProcessInstallRoots;
}
return buildWindowsInstallRoots(env, false);
}
export function getWindowsProgramFilesRoots(
env: Record<string, string | undefined> = process.env,
): readonly string[] {
const roots = getWindowsInstallRoots(env);
const seen = new Set<string>();
const result: string[] = [];
for (const value of [roots.programW6432, roots.programFiles, roots.programFilesX86]) {
if (!value) {
continue;
}
const key = value.toLowerCase();
if (seen.has(key)) {
continue;
}
seen.add(key);
result.push(value);
}
return result;
}
export function _resetWindowsInstallRootsForTests(
overrides: WindowsInstallRootsTestOverrides = {},
): void {
queryRegistryValueFn = overrides.queryRegistryValue ?? defaultQueryRegistryValue;
isReadableFileFn = overrides.isReadableFile ?? defaultIsReadableFile;
cachedProcessInstallRoots = null;
}
export const _private = {
getWindowsRegExeCandidates,
locateWindowsRegExe,
};