From c8f466030cceb0bba2967ef84727980d8e0d1396 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 21:01:50 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]?= =?UTF-8?q?=20Fix=20execa=20environment=20variable=20leakage?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .jules/sentinel.md | 5 +++++ src/cli/authorization/non-interactive.ts | 3 +++ src/core/tools/capability/runner.ts | 5 ++++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/.jules/sentinel.md b/.jules/sentinel.md index 1446dff3..7f7617f9 100644 --- a/.jules/sentinel.md +++ b/.jules/sentinel.md @@ -12,3 +12,8 @@ **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-10-24 - Fix execa extendEnv for missing sanitizeEnvironment +**Vulnerability:** Found more instances where execa is called with `extendEnv: false` missing and process.env is exposed directly via the default extendEnv, specifically in runner capabilities and CLI auth scripts. +**Learning:** `execa` defaulting to `extendEnv: true` is a very dangerous default since we have secret env keys. +**Prevention:** Make sure all references to `execa` get `extendEnv: false` combined with `sanitizeEnvironment`. diff --git a/src/cli/authorization/non-interactive.ts b/src/cli/authorization/non-interactive.ts index 369e269b..c67182f1 100644 --- a/src/cli/authorization/non-interactive.ts +++ b/src/cli/authorization/non-interactive.ts @@ -13,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 @@ -101,6 +102,8 @@ export async function requestNonInteractiveAuthorizationDecision(params: { shell: true, timeout: timeoutMs, reject: false, + env: sanitizeEnvironment(process.env), + extendEnv: false, }); if (typeof res.exitCode === 'number' && res.exitCode !== 0) { diff --git a/src/core/tools/capability/runner.ts b/src/core/tools/capability/runner.ts index dbba2080..91c71c9d 100644 --- a/src/core/tools/capability/runner.ts +++ b/src/core/tools/capability/runner.ts @@ -1,5 +1,7 @@ import { execa } from 'execa'; +import { sanitizeEnvironment } from '../../utils/sanitizer.js'; + import { ExecOpts, ExecResult } from './types.js'; /** @@ -15,7 +17,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 8f34bd230753b67a1fed5e3a6ef7fbc377465c2b Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Wed, 2 Sep 2026 22:21:55 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=9B=A1=EF=B8=8F=20Sentinel:=20[HIGH]?= =?UTF-8?q?=20Fix=20mock.clearAllMocks=20error=20in=20tests=20and=20execa?= =?UTF-8?q?=20environment=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/cli/authorization/non-interactive.ts | 2 +- src/core/facades/cli-authorization-non-interactive.ts | 1 + tests/helpers/bun-test-harness.ts | 4 +++- 3 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/cli/authorization/non-interactive.ts b/src/cli/authorization/non-interactive.ts index c67182f1..9800a5bd 100644 --- a/src/cli/authorization/non-interactive.ts +++ b/src/cli/authorization/non-interactive.ts @@ -11,9 +11,9 @@ import type { import { getLogger, McpConnectionManager, + 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'; diff --git a/tests/helpers/bun-test-harness.ts b/tests/helpers/bun-test-harness.ts index abd43ad6..97e193bf 100644 --- a/tests/helpers/bun-test-harness.ts +++ b/tests/helpers/bun-test-harness.ts @@ -142,7 +142,9 @@ export function restoreConsoleOutputs() { export function clearMockState() { mock.restore(); - mock.clearAllMocks(); + if (typeof mock.clearAllMocks === 'function') { + mock.clearAllMocks(); + } auditTrail.clearAuditTrail(); }