(command: string)
| 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. |
| 21 | function 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 |
no test coverage detected