From 7a54fd7d23f539f559d8072ba7d5bc8b628aecbb Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:11:50 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20f?= =?UTF-8?q?ramework=20secrets=20leakage=20via=20execa=20extendEnv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Environment variable redaction via `sanitizeEnvironment` is bypassed when calling `execa` without `extendEnv: false`, because it defaults to `true` and re-injects sensitive secrets from `process.env`. This commit fixes `src/core/tools/capability/runner.ts` and `src/cli/authorization/non-interactive.ts` to properly pass `extendEnv: false` while merging custom `env` with `process.env` so that `PATH` is not stripped. --- .jules/sentinel.md | 4 ++++ src/cli/authorization/non-interactive.ts | 3 +++ src/core/tools/capability/runner.ts | 4 +++- 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1446dff3..9e751ef5 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -12,3 +12,7 @@ **Vulnerability:** The redaction regex `KV_PATTERN` failed to match and redact quoted secret values (e.g., `password="mysecret"`), potentially leaking credentials in audit logs. **Learning:** Regular expressions for sanitizing key=value pairs must account for quoted values by explicitly including `"[^"]*"` and `'[^']*'` in the matching group. **Prevention:** When writing regex for secrets matching, always include patterns for both quoted and unquoted strings to prevent simple bypasses. +## 2026-08-30 - Fix framework secrets leakage via execa extendEnv +**Vulnerability:** execa re-injects unsanitized process.env by default when extendEnv is true. +**Learning:** Using sanitizeEnvironment() but missing extendEnv: false defeated the purpose. When setting `extendEnv: false`, we must also merge `process.env` with `opts.env` (e.g. `opts.env ? { ...process.env, ...opts.env } : process.env`), otherwise `PATH` and other essential system variables are stripped, causing ENOENT crashes. +**Prevention:** Always explicitly set extendEnv: false when passing a sanitized environment object to execa, but ensure to merge the base system environment. diff --git a/src/cli/authorization/non-interactive.ts b/src/cli/authorization/non-interactive.ts index 369e269b..721ff5b5 100644 --- a/src/cli/authorization/non-interactive.ts +++ b/src/cli/authorization/non-interactive.ts @@ -1,6 +1,7 @@ import { execa } from 'execa'; import { z } from 'zod'; +import { sanitizeEnvironment } from '../../core/utils/sanitizer.js'; import type { AuthorizationDecision, ResolvedExtensions, @@ -100,6 +101,8 @@ export async function requestNonInteractiveAuthorizationDecision(params: { input: JSON.stringify({ request: params.request }), shell: true, timeout: timeoutMs, + env: sanitizeEnvironment(process.env), + extendEnv: false, reject: false, }); diff --git a/src/core/tools/capability/runner.ts b/src/core/tools/capability/runner.ts index dbba2080..d2e20ba2 100644 --- a/src/core/tools/capability/runner.ts +++ b/src/core/tools/capability/runner.ts @@ -1,5 +1,6 @@ import { execa } from 'execa'; +import { sanitizeEnvironment } from '../../utils/sanitizer.js'; import { ExecOpts, ExecResult } from './types.js'; /** @@ -15,7 +16,8 @@ export function createControlledRunner() { cwd: opts?.cwd, timeout: opts?.timeoutMs, maxBuffer: opts?.maxStdoutBytes, - env: opts?.env, + env: sanitizeEnvironment(opts?.env ? { ...process.env, ...opts.env } : process.env), + extendEnv: false, reject: false, // Backends should handle exit codes themselves }); From a6465a5405c80e93024c9badd79812cb502573e9 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:20:56 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20f?= =?UTF-8?q?ramework=20secrets=20leakage=20via=20execa=20extendEnv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Environment variable redaction via `sanitizeEnvironment` is bypassed when calling `execa` without `extendEnv: false`, because it defaults to `true` and re-injects sensitive secrets from `process.env`. This commit fixes `src/core/tools/capability/runner.ts` and `src/cli/authorization/non-interactive.ts` to properly pass `extendEnv: false` while merging custom `env` with `process.env` so that `PATH` is not stripped. This also includes the linting fixes applied automatically by eslint/oxfmt. --- src/cli/authorization/non-interactive.ts | 2 +- src/core/tools/capability/runner.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/authorization/non-interactive.ts b/src/cli/authorization/non-interactive.ts index 721ff5b5..ed339e5d 100644 --- a/src/cli/authorization/non-interactive.ts +++ b/src/cli/authorization/non-interactive.ts @@ -1,7 +1,6 @@ import { execa } from 'execa'; import { z } from 'zod'; -import { sanitizeEnvironment } from '../../core/utils/sanitizer.js'; import type { AuthorizationDecision, ResolvedExtensions, @@ -14,6 +13,7 @@ import { McpConnectionManager, } from '../../core/facades/cli-authorization-non-interactive.js'; import { isRecord } from '../../core/facades/cli-utils-serialize.js'; +import { sanitizeEnvironment } from '../../core/utils/sanitizer.js'; import { text } from '../locales/index.js'; const DecisionSchema = z diff --git a/src/core/tools/capability/runner.ts b/src/core/tools/capability/runner.ts index d2e20ba2..91c71c9d 100644 --- a/src/core/tools/capability/runner.ts +++ b/src/core/tools/capability/runner.ts @@ -1,6 +1,7 @@ import { execa } from 'execa'; import { sanitizeEnvironment } from '../../utils/sanitizer.js'; + import { ExecOpts, ExecResult } from './types.js'; /** From f448a05420084626a8cd6d6a60fe5425c8fc7971 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Tue, 1 Sep 2026 21:30:04 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20Fix=20f?= =?UTF-8?q?ramework=20secrets=20leakage=20via=20execa=20extendEnv?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Environment variable redaction via `sanitizeEnvironment` is bypassed when calling `execa` without `extendEnv: false`, because it defaults to `true` and re-injects sensitive secrets from `process.env`. This commit fixes `src/core/tools/capability/runner.ts` and `src/cli/authorization/non-interactive.ts` to properly pass `extendEnv: false` while merging custom `env` with `process.env` so that `PATH` is not stripped. This also includes the linting fixes applied automatically by eslint/oxfmt, and respects architectural boundaries by importing `sanitizeEnvironment` through the facade in CLI code. --- src/cli/authorization/non-interactive.ts | 2 +- src/core/facades/cli-authorization-non-interactive.ts | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/cli/authorization/non-interactive.ts b/src/cli/authorization/non-interactive.ts index ed339e5d..808acf90 100644 --- a/src/cli/authorization/non-interactive.ts +++ b/src/cli/authorization/non-interactive.ts @@ -12,8 +12,8 @@ import { getLogger, McpConnectionManager, } from '../../core/facades/cli-authorization-non-interactive.js'; +import { sanitizeEnvironment } from '../../core/facades/cli-authorization-non-interactive.js'; import { isRecord } from '../../core/facades/cli-utils-serialize.js'; -import { sanitizeEnvironment } from '../../core/utils/sanitizer.js'; import { text } from '../locales/index.js'; const DecisionSchema = z diff --git a/src/core/facades/cli-authorization-non-interactive.ts b/src/core/facades/cli-authorization-non-interactive.ts index 63dd39f2..deb8a1df 100644 --- a/src/core/facades/cli-authorization-non-interactive.ts +++ b/src/core/facades/cli-authorization-non-interactive.ts @@ -6,3 +6,4 @@ export type { AuthorizationDecision, ToolAuthorizationRequest, } from '../tools/authorization/types.js'; +export { sanitizeEnvironment } from '../utils/sanitizer.js';