source dump of claude code
at main 153 lines 5.2 kB view raw
1import { getFeatureValue_CACHED_MAY_BE_STALE } from 'src/services/analytics/growthbook.js' 2import { splitCommand_DEPRECATED } from '../../utils/bash/commands.js' 3import { SandboxManager } from '../../utils/sandbox/sandbox-adapter.js' 4import { getSettings_DEPRECATED } from '../../utils/settings/settings.js' 5import { 6 BINARY_HIJACK_VARS, 7 bashPermissionRule, 8 matchWildcardPattern, 9 stripAllLeadingEnvVars, 10 stripSafeWrappers, 11} from './bashPermissions.js' 12 13type SandboxInput = { 14 command?: string 15 dangerouslyDisableSandbox?: boolean 16} 17 18// NOTE: excludedCommands is a user-facing convenience feature, not a security boundary. 19// It is not a security bug to be able to bypass excludedCommands — the sandbox permission 20// system (which prompts users) is the actual security control. 21function containsExcludedCommand(command: string): boolean { 22 // Check dynamic config for disabled commands and substrings (only for ants) 23 if (process.env.USER_TYPE === 'ant') { 24 const disabledCommands = getFeatureValue_CACHED_MAY_BE_STALE<{ 25 commands: string[] 26 substrings: string[] 27 }>('tengu_sandbox_disabled_commands', { commands: [], substrings: [] }) 28 29 // Check if command contains any disabled substrings 30 for (const substring of disabledCommands.substrings) { 31 if (command.includes(substring)) { 32 return true 33 } 34 } 35 36 // Check if command starts with any disabled commands 37 try { 38 const commandParts = splitCommand_DEPRECATED(command) 39 for (const part of commandParts) { 40 const baseCommand = part.trim().split(' ')[0] 41 if (baseCommand && disabledCommands.commands.includes(baseCommand)) { 42 return true 43 } 44 } 45 } catch { 46 // If we can't parse the command (e.g., malformed bash syntax), 47 // treat it as not excluded to allow other validation checks to handle it 48 // This prevents crashes when rendering tool use messages 49 } 50 } 51 52 // Check user-configured excluded commands from settings 53 const settings = getSettings_DEPRECATED() 54 const userExcludedCommands = settings.sandbox?.excludedCommands ?? [] 55 56 if (userExcludedCommands.length === 0) { 57 return false 58 } 59 60 // Split compound commands (e.g. "docker ps && curl evil.com") into individual 61 // subcommands and check each one against excluded patterns. This prevents a 62 // compound command from escaping the sandbox just because its first subcommand 63 // matches an excluded pattern. 64 let subcommands: string[] 65 try { 66 subcommands = splitCommand_DEPRECATED(command) 67 } catch { 68 subcommands = [command] 69 } 70 71 for (const subcommand of subcommands) { 72 const trimmed = subcommand.trim() 73 // Also try matching with env var prefixes and wrapper commands stripped, so 74 // that `FOO=bar bazel ...` and `timeout 30 bazel ...` match `bazel:*`. Not a 75 // security boundary (see NOTE at top); the &&-split above already lets 76 // `export FOO=bar && bazel ...` match. BINARY_HIJACK_VARS kept as a heuristic. 77 // 78 // We iteratively apply both stripping operations until no new candidates are 79 // produced (fixed-point), matching the approach in filterRulesByContentsMatchingInput. 80 // This handles interleaved patterns like `timeout 300 FOO=bar bazel run` 81 // where single-pass composition would fail. 82 const candidates = [trimmed] 83 const seen = new Set(candidates) 84 let startIdx = 0 85 while (startIdx < candidates.length) { 86 const endIdx = candidates.length 87 for (let i = startIdx; i < endIdx; i++) { 88 const cmd = candidates[i]! 89 const envStripped = stripAllLeadingEnvVars(cmd, BINARY_HIJACK_VARS) 90 if (!seen.has(envStripped)) { 91 candidates.push(envStripped) 92 seen.add(envStripped) 93 } 94 const wrapperStripped = stripSafeWrappers(cmd) 95 if (!seen.has(wrapperStripped)) { 96 candidates.push(wrapperStripped) 97 seen.add(wrapperStripped) 98 } 99 } 100 startIdx = endIdx 101 } 102 103 for (const pattern of userExcludedCommands) { 104 const rule = bashPermissionRule(pattern) 105 for (const cand of candidates) { 106 switch (rule.type) { 107 case 'prefix': 108 if (cand === rule.prefix || cand.startsWith(rule.prefix + ' ')) { 109 return true 110 } 111 break 112 case 'exact': 113 if (cand === rule.command) { 114 return true 115 } 116 break 117 case 'wildcard': 118 if (matchWildcardPattern(rule.pattern, cand)) { 119 return true 120 } 121 break 122 } 123 } 124 } 125 } 126 127 return false 128} 129 130export function shouldUseSandbox(input: Partial<SandboxInput>): boolean { 131 if (!SandboxManager.isSandboxingEnabled()) { 132 return false 133 } 134 135 // Don't sandbox if explicitly overridden AND unsandboxed commands are allowed by policy 136 if ( 137 input.dangerouslyDisableSandbox && 138 SandboxManager.areUnsandboxedCommandsAllowed() 139 ) { 140 return false 141 } 142 143 if (!input.command) { 144 return false 145 } 146 147 // Don't sandbox if the command contains user-configured excluded commands 148 if (containsExcludedCommand(input.command)) { 149 return false 150 } 151 152 return true 153}