Skip to content

Commit c422f19

Browse files
committed
fix: share git inherited env sanitization
1 parent 5f691df commit c422f19

3 files changed

Lines changed: 68 additions & 26 deletions

File tree

src/agents/bash-tools.exec-runtime.test.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ let formatExecFailureReason: typeof import("./bash-tools.exec-runtime.js").forma
3434
let renderExecUpdateText: typeof import("./bash-tools.exec-runtime.js").renderExecUpdateText;
3535
let resolveExecTarget: typeof import("./bash-tools.exec-runtime.js").resolveExecTarget;
3636
let runExecProcess: typeof import("./bash-tools.exec-runtime.js").runExecProcess;
37+
let sanitizeHostBaseEnv: typeof import("./bash-tools.exec-runtime.js").sanitizeHostBaseEnv;
3738

3839
beforeAll(async () => {
3940
({ markBackgrounded } = await import("./bash-process-registry.js"));
@@ -45,6 +46,7 @@ beforeAll(async () => {
4546
renderExecUpdateText,
4647
resolveExecTarget,
4748
runExecProcess,
49+
sanitizeHostBaseEnv,
4850
} = await import("./bash-tools.exec-runtime.js"));
4951
});
5052

@@ -113,6 +115,33 @@ describe("detectCursorKeyMode", () => {
113115
});
114116
});
115117

118+
describe("sanitizeHostBaseEnv", () => {
119+
it("uses value-aware Git protocol inherited env sanitization", () => {
120+
expect(
121+
sanitizeHostBaseEnv({
122+
PATH: "/usr/bin:/bin",
123+
GIT_ALLOW_PROTOCOL: "https:ext:ssh",
124+
GIT_PROTOCOL_FROM_USER: "1",
125+
GIT_SSH_COMMAND: "touch /tmp/pwned",
126+
SAFE: "ok",
127+
}),
128+
).toEqual({
129+
PATH: "/usr/bin:/bin",
130+
GIT_ALLOW_PROTOCOL: "https:ssh",
131+
GIT_PROTOCOL_FROM_USER: "0",
132+
SAFE: "ok",
133+
});
134+
135+
expect(
136+
sanitizeHostBaseEnv({
137+
GIT_PROTOCOL_FROM_USER: "false",
138+
}),
139+
).toEqual({
140+
GIT_PROTOCOL_FROM_USER: "false",
141+
});
142+
});
143+
});
144+
116145
describe("resolveExecTarget", () => {
117146
it("keeps implicit auto on sandbox when a sandbox runtime is available", () => {
118147
expectExecTarget(

src/agents/bash-tools.exec-runtime.ts

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,10 @@ import {
1919
type ExecTarget,
2020
} from "../infra/exec-approvals.js";
2121
import { requestHeartbeat } from "../infra/heartbeat-wake.js";
22-
import { isDangerousHostInheritedEnvVarName } from "../infra/host-env-security.js";
22+
import {
23+
isDangerousHostInheritedEnvVarName,
24+
sanitizeHostInheritedEnvEntry,
25+
} from "../infra/host-env-security.js";
2326
import { findPathKey, mergePathPrepend, removePathPrepend } from "../infra/path-prepend.js";
2427
import { enqueueSystemEvent } from "../infra/system-events.js";
2528
import { isSubagentSessionKey } from "../sessions/session-key-utils.js";
@@ -93,15 +96,12 @@ export function detectCursorKeyMode(raw: string): "application" | "normal" | nul
9396
export function sanitizeHostBaseEnv(env: Record<string, string>): Record<string, string> {
9497
const sanitized: Record<string, string> = {};
9598
for (const [key, value] of Object.entries(env)) {
96-
const upperKey = key.toUpperCase();
97-
if (upperKey === "PATH") {
98-
sanitized[key] = value;
99-
continue;
100-
}
101-
if (isDangerousHostInheritedEnvVarName(upperKey)) {
99+
const sanitizedEntry = sanitizeHostInheritedEnvEntry(key, value);
100+
if (!sanitizedEntry) {
102101
continue;
103102
}
104-
sanitized[key] = value;
103+
const [sanitizedKey, sanitizedValue] = sanitizedEntry;
104+
sanitized[sanitizedKey] = sanitizedValue;
105105
}
106106
return sanitized;
107107
}

src/infra/host-env-security.ts

Lines changed: 31 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -174,6 +174,33 @@ function sanitizeInheritedGitAllowProtocolValue(value: string): string {
174174
return safeProtocols.join(":");
175175
}
176176

177+
export function sanitizeHostInheritedEnvEntry(
178+
rawKey: string,
179+
value: string,
180+
): [string, string] | null {
181+
const key = normalizeEnvVarKey(rawKey);
182+
if (!key) {
183+
return null;
184+
}
185+
// Preserve inherited Git allowlists without widening malformed or unsafe entries by deletion.
186+
// Protocols outside Git's safe default set are removed instead of being passed through.
187+
if (key.toUpperCase() === GIT_ALLOW_PROTOCOL_ENV_KEY) {
188+
return [key, sanitizeInheritedGitAllowProtocolValue(value)];
189+
}
190+
// Preserve non-permissive Git boolean values. Permissive values must become explicit `0`
191+
// because Git's unset default still permits protocols with policy `user`.
192+
if (key.toUpperCase() === GIT_PROTOCOL_FROM_USER_ENV_KEY) {
193+
return [
194+
key,
195+
isPermissiveGitProtocolFromUserValue(value) ? GIT_PROTOCOL_FROM_USER_DISABLED_VALUE : value,
196+
];
197+
}
198+
if (isDangerousHostInheritedEnvVarName(key)) {
199+
return null;
200+
}
201+
return [key, value];
202+
}
203+
177204
function sanitizeHostEnvOverridesWithDiagnostics(params?: {
178205
overrides?: Record<string, string> | null;
179206
blockPathOverrides?: boolean;
@@ -236,26 +263,12 @@ export function sanitizeHostExecEnvWithDiagnostics(params?: {
236263

237264
const merged: Record<string, string> = {};
238265
for (const [key, value] of listNormalizedEnvEntries(baseEnv)) {
239-
// Preserve inherited Git allowlists without widening malformed or unsafe entries by deletion.
240-
// Protocols outside Git's safe default set are removed instead of being passed through.
241-
if (key.toUpperCase() === GIT_ALLOW_PROTOCOL_ENV_KEY) {
242-
merged[key] = sanitizeInheritedGitAllowProtocolValue(value);
243-
continue;
244-
}
245-
// Preserve non-permissive Git boolean values. Permissive values must become explicit `0`
246-
// because Git's unset default still permits protocols with policy `user`.
247-
if (key.toUpperCase() === GIT_PROTOCOL_FROM_USER_ENV_KEY) {
248-
if (!isPermissiveGitProtocolFromUserValue(value)) {
249-
merged[key] = value;
250-
} else {
251-
merged[key] = GIT_PROTOCOL_FROM_USER_DISABLED_VALUE;
252-
}
253-
continue;
254-
}
255-
if (isDangerousHostInheritedEnvVarName(key)) {
266+
const sanitizedEntry = sanitizeHostInheritedEnvEntry(key, value);
267+
if (!sanitizedEntry) {
256268
continue;
257269
}
258-
merged[key] = value;
270+
const [sanitizedKey, sanitizedValue] = sanitizedEntry;
271+
merged[sanitizedKey] = sanitizedValue;
259272
}
260273

261274
const overrideResult = sanitizeHostEnvOverridesWithDiagnostics({

0 commit comments

Comments
 (0)