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
154
|
package sandbox
import (
"context"
"fmt"
"path"
"regexp"
"strings"
)
// DefaultBashDenylist is the built-in set of dangerous command patterns
// DenylistBashHook rejects. It intentionally stays narrow: a false positive
// on an ordinary dev workflow (git status, go test ./..., ls, rm of a
// specific file, chmod 644, ...) is worse than missing an exotic bypass,
// because a rejection is not fatal (see RejectionError) — the model reads
// the reason and can still get equivalent work done another way. This list
// is a speed bump against obviously catastrophic or exfiltration-flavored
// commands, not a security boundary; the sandbox's own isolation (container
// or host path confinement) is what actually contains a run.
var DefaultBashDenylist = []*regexp.Regexp{
// rm -rf / or rm -rf /* — wipe the filesystem root. Deliberately scoped
// to "/" or "/*" as the target so "rm -rf /workspace/tmp" (a normal,
// scoped cleanup) is not caught.
regexp.MustCompile(`\brm\s+(-[a-zA-Z]*[rf][a-zA-Z]*[rf][a-zA-Z]*|--recursive\s+--force|--force\s+--recursive)\s+/(\s|\*|$)`),
// force-push to a remote.
regexp.MustCompile(`\bgit\s+push\b[^\n]*(--force\b|(^|\s)-f(\s|$))`),
// pipe a remote script straight into a shell.
regexp.MustCompile(`\bcurl\b[^|\n]*\|\s*(sudo\s+)?(sh|bash|zsh)\b`),
regexp.MustCompile(`\bwget\b[^|\n]*\|\s*(sudo\s+)?(sh|bash|zsh)\b`),
// privilege escalation.
regexp.MustCompile(`(^|[;&|]\s*)sudo\s`),
// world-writable permission grants.
regexp.MustCompile(`\bchmod\s+(-R\s+)?0?777\b`),
// raw disk writes.
regexp.MustCompile(`\bdd\s+if=`),
}
// DenylistBashHook rejects run_bash commands matching any of Patterns
// (DefaultBashDenylist if nil). It never inspects write_file calls.
type DenylistBashHook struct {
// Patterns overrides DefaultBashDenylist when non-nil, so callers can
// tune the list without editing this file.
Patterns []*regexp.Regexp
}
func (h *DenylistBashHook) patterns() []*regexp.Regexp {
if h.Patterns != nil {
return h.Patterns
}
return DefaultBashDenylist
}
func (h *DenylistBashHook) CheckBash(_ context.Context, command string) error {
for _, p := range h.patterns() {
if p.MatchString(command) {
return fmt.Errorf("command matches denylisted pattern %q", p.String())
}
}
return nil
}
// CheckWrite is a no-op; DenylistBashHook only gates run_bash.
func (h *DenylistBashHook) CheckWrite(context.Context, string) error { return nil }
var _ Hook = (*DenylistBashHook)(nil)
// DefaultProtectedPathPatterns is the built-in set of path patterns
// ProtectedPathHook rejects writes to. A pattern ending in "/**" protects
// that directory and everything under it at any depth (path.Match has no
// cross-slash wildcard, so this suffix is handled specially — see
// matchProtectedPattern); any other pattern is matched with path.Match
// (shell-glob semantics: "*" matches within one path segment).
//
// Patterns are checked against every path suffix of the write target
// (i.e. at every "/"-boundary), so ".git/**" rejects both ".git/config" and
// "nested/project/.git/config", not just a top-level ".git".
var DefaultProtectedPathPatterns = []string{
".git/**",
".env",
".env.*",
".github/workflows/**",
}
// DefaultProtectedPathSubstrings is matched as a plain substring of the
// (cleaned, slash-normalized) write path — for patterns glob syntax can't
// express, like "any path containing a credentials/ directory".
var DefaultProtectedPathSubstrings = []string{
"credentials/",
}
// ProtectedPathHook rejects write_file calls whose path matches any of
// Patterns/Substrings (DefaultProtectedPathPatterns/
// DefaultProtectedPathSubstrings if nil). It never inspects run_bash
// commands — a determined agent can still edit a protected file via
// run_bash (echo/sed/etc.); that's accepted for this phase, see
// internal/sandbox/dockersandbox.go and hostsandbox.go doc comments on the
// overall trust model. ProtectedPathHook exists to stop the common case: a
// model calling write_file directly on something like .env or
// .git/config.
type ProtectedPathHook struct {
Patterns []string
Substrings []string
}
func (h *ProtectedPathHook) patterns() []string {
if h.Patterns != nil {
return h.Patterns
}
return DefaultProtectedPathPatterns
}
func (h *ProtectedPathHook) substrings() []string {
if h.Substrings != nil {
return h.Substrings
}
return DefaultProtectedPathSubstrings
}
func (h *ProtectedPathHook) CheckWrite(_ context.Context, p string) error {
clean := strings.TrimPrefix(path.Clean(strings.ReplaceAll(p, "\\", "/")), "/")
for _, sub := range h.substrings() {
if strings.Contains(clean, sub) {
return fmt.Errorf("path %q contains protected substring %q", p, sub)
}
}
parts := strings.Split(clean, "/")
for i := range parts {
suffix := strings.Join(parts[i:], "/")
for _, pat := range h.patterns() {
if matchProtectedPattern(pat, suffix) {
return fmt.Errorf("path %q matches protected pattern %q", p, pat)
}
}
}
return nil
}
// CheckBash is a no-op; ProtectedPathHook only gates write_file.
func (h *ProtectedPathHook) CheckBash(context.Context, string) error { return nil }
var _ Hook = (*ProtectedPathHook)(nil)
// matchProtectedPattern matches name against pat. A pat ending in "/**"
// means "this directory or anything beneath it"; any other pat uses
// path.Match's shell-glob semantics ("*" bounded by "/").
func matchProtectedPattern(pat, name string) bool {
if prefix, ok := strings.CutSuffix(pat, "/**"); ok {
return name == prefix || strings.HasPrefix(name, prefix+"/")
}
ok, _ := path.Match(pat, name)
return ok
}
|