Skip to content

Commit 8b2bf7b

Browse files
authored
Harden update environment path resolution (#77470)
* Harden update environment path resolution * docs(changelog): credit windows update env path hardening Adds the user-facing Unreleased Fixes entry for the workspace LOCALAPPDATA blocklist + portable Git path-prepend hardening change in this PR.
1 parent f368201 commit 8b2bf7b

5 files changed

Lines changed: 54 additions & 5 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@ Docs: https://docs.openclaw.ai
210210
- Security/Windows: validate `SystemRoot`/`WINDIR` env values through the Windows install-root validator and add them to the dangerous-host-env policy when resolving `icacls.exe`/`whoami.exe` for `openclaw security audit`, so workspace `.env` overrides and bare command names cannot redirect Windows ACL helpers to attacker-controlled binaries. (#74458) Thanks @mmaps.
211211
- Security/Windows: pin Windows registry-probe `reg.exe` resolution to the canonical Windows install root in install-root probing, so `SystemRoot`/`WINDIR` env overrides cannot redirect registry queries during Windows host detection. (#74454) Thanks @mmaps.
212212
- QQBot: preserve the framework command authorization decision when converting framework command contexts into engine slash command contexts, so downstream slash handlers see `commandAuthorized` matching the channel's resolved `isAuthorizedSender` instead of a hardcoded `true`. (#77453) Thanks @drobison00.
213+
- Security/Windows: block `LOCALAPPDATA` from workspace `.env` and resolve Windows update-flow portable Git path prepends from the trusted process-local `LOCALAPPDATA` only, so workspace-supplied values cannot redirect `git` discovery during `openclaw update`. (#77470) Thanks @drobison00.
213214

214215
## 2026.5.3-1
215216

src/infra/dotenv.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,8 @@ const BUNDLED_TRUST_ROOT_ENV_KEYS = BUNDLED_TRUST_ROOT_ENV_LINES.map(
4444
const WINDOWS_SHELL_TRUST_ROOT_ENV_KEYS = [
4545
"ComSpec",
4646
"COMSPEC",
47+
"LocalAppData",
48+
"LOCALAPPDATA",
4749
"ProgramFiles",
4850
"PROGRAMFILES",
4951
"ProgramW6432",
@@ -338,6 +340,8 @@ describe("loadDotEnv", () => {
338340
[
339341
"ComSpec=.\\evil-comspec",
340342
"COMSPEC=.\\evil-comspec-upper",
343+
"LocalAppData=.\\evil-local-app-data",
344+
"LOCALAPPDATA=.\\evil-local-app-data-upper",
341345
"ProgramFiles=.\\evil-pfiles",
342346
"PROGRAMFILES=.\\evil-pfiles-upper",
343347
"ProgramW6432=.\\evil-pw6432",
@@ -715,6 +719,7 @@ describe("workspace .env blocklist completeness", () => {
715719
"HOMEBREW_BREW_FILE",
716720
"HOMEBREW_PREFIX",
717721
"IRC_HOST",
722+
"LOCALAPPDATA",
718723
"MATTERMOST_URL",
719724
"MATRIX_HOMESERVER",
720725
"MINIMAX_API_HOST",

src/infra/dotenv.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ const BLOCKED_WORKSPACE_DOTENV_KEYS = new Set([
2929
"HOMEBREW_BREW_FILE",
3030
"HOMEBREW_PREFIX",
3131
"IRC_HOST",
32+
"LOCALAPPDATA",
3233
"MATTERMOST_URL",
3334
"MATRIX_HOMESERVER",
3435
"MINIMAX_API_HOST",

src/infra/update-global.test.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,50 @@ describe("update global helpers", () => {
150150
});
151151
});
152152

153+
it("resolves portable Git paths from process-local app data only", async () => {
154+
const platformSpy = vi.spyOn(process, "platform", "get").mockReturnValue("win32");
155+
try {
156+
await withTempDir({ prefix: "openclaw-update-portable-git-" }, async (base) => {
157+
envSnapshot = captureEnv(["LOCALAPPDATA"]);
158+
const injectedLocalAppData = path.join(base, "injected-local-app-data");
159+
const trustedLocalAppData = path.join(base, "trusted-local-app-data");
160+
const injectedGitDir = path.join(
161+
injectedLocalAppData,
162+
"OpenClaw",
163+
"deps",
164+
"portable-git",
165+
"cmd",
166+
);
167+
const trustedGitDir = path.join(
168+
trustedLocalAppData,
169+
"OpenClaw",
170+
"deps",
171+
"portable-git",
172+
"cmd",
173+
);
174+
await fs.mkdir(injectedGitDir, { recursive: true });
175+
await fs.mkdir(trustedGitDir, { recursive: true });
176+
177+
delete process.env.LOCALAPPDATA;
178+
const injectedOnlyEnv = await createGlobalInstallEnv({
179+
LOCALAPPDATA: injectedLocalAppData,
180+
PATH: "base-bin",
181+
});
182+
expect(injectedOnlyEnv?.PATH).not.toContain(injectedGitDir);
183+
184+
process.env.LOCALAPPDATA = trustedLocalAppData;
185+
const trustedEnv = await createGlobalInstallEnv({
186+
LOCALAPPDATA: injectedLocalAppData,
187+
PATH: "base-bin",
188+
});
189+
expect(trustedEnv?.PATH).toContain(trustedGitDir);
190+
expect(trustedEnv?.PATH).not.toContain(injectedGitDir);
191+
});
192+
} finally {
193+
platformSpy.mockRestore();
194+
}
195+
});
196+
153197
it("classifies main and raw install specs separately from registry selectors", () => {
154198
expect(isMainPackageTarget("main")).toBe(true);
155199
expect(isMainPackageTarget(" MAIN ")).toBe(true);

src/infra/update-global.ts

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -274,13 +274,11 @@ export function canResolveRegistryVersionForPackageTarget(value: string): boolea
274274
return !isMainPackageTarget(trimmed) && !isExplicitPackageInstallSpec(trimmed);
275275
}
276276

277-
async function resolvePortableGitPathPrepend(
278-
env: NodeJS.ProcessEnv | undefined,
279-
): Promise<string[]> {
277+
async function resolvePortableGitPathPrepend(): Promise<string[]> {
280278
if (process.platform !== "win32") {
281279
return [];
282280
}
283-
const localAppData = env?.LOCALAPPDATA?.trim() || process.env.LOCALAPPDATA?.trim();
281+
const localAppData = process.env.LOCALAPPDATA?.trim();
284282
if (!localAppData) {
285283
return [];
286284
}
@@ -341,7 +339,7 @@ export function resolveGlobalInstallSpec(params: {
341339
export async function createGlobalInstallEnv(
342340
env?: NodeJS.ProcessEnv,
343341
): Promise<NodeJS.ProcessEnv | undefined> {
344-
const pathPrepend = await resolvePortableGitPathPrepend(env);
342+
const pathPrepend = await resolvePortableGitPathPrepend();
345343
const sourceEnv = env ?? process.env;
346344
const hasCorepackDownloadPromptSetting = Boolean(
347345
sourceEnv.COREPACK_ENABLE_DOWNLOAD_PROMPT?.trim(),

0 commit comments

Comments
 (0)