Root cause
Authentication & access control introduced
asdf
How AI contributed: Direct introductionPublished Apr 21, 2026 in
gitlawb/openclaude
Sandbox bypass via early-exit logic flaw (CWE-284/CWE-22) in bashToolHasPermission() in src/tools/BashTool/bashPermissions.ts: when sandboxing + auto-allow-bash are enabled and no explicit deny rule matches, checkSandboxAutoAllow returns behavior 'allow' and the block 'if (sandboxAutoAllowResult.behavior !== "passthrough") return sandboxAutoAllowResult' returns immediately, so the path-constraint filter checkPathConstraints (which runs later in the same function) is never evaluated; commands with path traversal (e.g. 'cat ../../../../../etc/passwd') bypass sandbox directory restrictions (read/write outside sandbox, cvss 8.4, npm @gitlawb/openclaude <0.5.1).
Root cause
asdf
Fix
fix: enforce Bash path constraints after sandbox allow (#777)
Fix by Kevin Codex · no AI marker found
Case facts
+ }++ // Legacy shell-quote pre-check. Only reached on 'parse-unavailable'+ // (tree-sitter not loaded OR TREE_SITTER_BASH feature gated off). Falls+ // through to the full legacy path below.+ if (astResult.kind === 'parse-unavailable') {+ logForDebugging(+ 'bashToolHasPermission: tree-sitter unavailable, using legacy shell-quote path',+ )+ const parseResult = tryParseShellCommand(input.command)+ if (!parseResult.success) {+ const decisionReason = {+ type: 'other' as const,+ reason: `Command contains malformed syntax that cannot be parsed: ${parseResult.error}`,+ }+ return {+ behavior: 'ask',+ decisionReason,+ message: createPermissionRequestMessage(BashTool.name, decisionReason),+ }+ }+ }++ // Check sandbox auto-allow (which respects explicit deny/ask rules)+ // Only call this if sandboxing and auto-allow are both enabled+ if (+ SandboxManager.isSandboxingEnabled() &&+ SandboxManager.isAutoAllowBashIfSandboxedEnabled() &&+ shouldUseSandbox(input)+ ) {+ const sandboxAutoAllowResult = checkSandboxAutoAllow(+ input,+ appState.toolPermissionContext,+ )+ if (sandboxAutoAllowResult.behavior !== 'passthrough') {+ return sandboxAutoAllowResult+ }+ }++ // Check exact match first+ const exactMatchResult = bashToolCheckExactMatchPermission(+ input,+ appState.toolPermissionContext,+ )++ // Exact command was denied+ if (exactMatchResult.behavior === 'deny') {+ return exactMatchResult+ }++ // Check Bash prompt deny and ask rules in parallel (both use Haiku).+ // Deny takes precedence over ask, and both take precedence over allow rules.+ // Skip when in auto mode - auto mode classifier handles all permission decisions+ if (+ isClassifierPermissionsEnabled() &&+ !(+ feature('TRANSCRIPT_CLASSIFIER') &&+ appState.toolPermissionContext.mode === 'auto'+ )+ ) {+ const denyDescriptions = getBashPromptDenyDescriptions(+ appState.toolPermissionContext,+ )+ const askDescriptions = getBashPromptAskDescriptions(+ appState.toolPermissionContext,+ )+ const hasDeny = denyDescriptions.length > 0+ const hasAsk = askDescriptions.length > 0++ if (hasDeny || hasAsk) {+ const [denyResult, askResult] = await Promise.all([+ hasDeny+ ? classifyBashCommand(+ input.command,+ getCwd(),+ denyDescriptions,+ 'deny',+ context.abortController.signal,+ context.options.isNonInteractiveSession,+ )+ : null,+ hasAsk+ ? classifyBashCommand(+ input.command,+ getCwd(),+ askDescriptions,+ 'ask',+ context.abortController.signal,+ context.options.isNonInteractiveSession,+ )@@ -0,0 +1,59 @@+import { afterEach, expect, test } from 'bun:test'++import { getEmptyToolPermissionContext } from '../../Tool.js'+import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js'+import { bashToolHasPermission } from './bashPermissions.js'++const originalSandboxMethods = {+ isSandboxingEnabled: SandboxManager.isSandboxingEnabled,+ isAutoAllowBashIfSandboxedEnabled:+ SandboxManager.isAutoAllowBashIfSandboxedEnabled,+ areUnsandboxedCommandsAllowed: SandboxManager.areUnsandboxedCommandsAllowed,+ getExcludedCommands: SandboxManager.getExcludedCommands,+}++afterEach(() => {+ SandboxManager.isSandboxingEnabled =+ originalSandboxMethods.isSandboxingEnabled+ SandboxManager.isAutoAllowBashIfSandboxedEnabled =+ originalSandboxMethods.isAutoAllowBashIfSandboxedEnabled+ SandboxManager.areUnsandboxedCommandsAllowed =+ originalSandboxMethods.areUnsandboxedCommandsAllowed+ SandboxManager.getExcludedCommands = originalSandboxMethods.getExcludedCommands+})++function makeToolUseContext() {+ const toolPermissionContext = getEmptyToolPermissionContext()++ return {+ abortController: new AbortController(),+ options: {+ isNonInteractiveSession: false,+ },+ getAppState() {+ return {+ toolPermissionContext,+ }+ },+ } as never+}++test('sandbox auto-allow still enforces Bash path constraints', async () => {+ ;(globalThis as unknown as { MACRO: { VERSION: string } }).MACRO = {+ VERSION: 'test',+ }++ SandboxManager.isSandboxingEnabled = () => true+ SandboxManager.isAutoAllowBashIfSandboxedEnabled = () => true+ SandboxManager.areUnsandboxedCommandsAllowed = () => true+ SandboxManager.getExcludedCommands = () => []++ const result = await bashToolHasPermission(+ { command: 'cat ../../../../../etc/passwd' },+ makeToolUseContext(),+ )++ expect(result.behavior).toBe('ask')+ expect(result.message).toContain('was blocked')+ expect(result.message).toContain('/etc/passwd')+})@@ -1814,7 +1814,10 @@ export async function bashToolHasPermission( input, appState.toolPermissionContext, )- if (sandboxAutoAllowResult.behavior !== 'passthrough') {+ if (+ sandboxAutoAllowResult.behavior === 'deny' ||+ sandboxAutoAllowResult.behavior === 'ask'+ ) { return sandboxAutoAllowResult } }AI-assisted change 321dade744776988f252de41944f92a93402f9e486d15002d8db8169ccdd3bfb · Fix 67a0edf20aa0fc8c46190fd1d926c22e3d3fd421e41da5d3f022f74e1cfdd977