Skip to content

Commit a882d2b

Browse files
committed
fix(network): harden Windows binary path matching
1 parent 80fcc58 commit a882d2b

9 files changed

Lines changed: 293 additions & 84 deletions

File tree

‎crates/openshell-supervisor-network/data/sandbox-policy.rego‎

Lines changed: 43 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -141,34 +141,70 @@ binary_allowed(_, _) if {
141141
not binary_identity_required
142142
}
143143

144+
# Binary path matching uses exact spelling by default. On Windows, Rust adds an
145+
# ASCII-folded candidate only after querying every parent directory of the
146+
# runtime-observed executable and confirming case-insensitive lookup throughout.
147+
binary_path_matches(binary, candidate) if {
148+
binary.path == candidate.path
149+
}
150+
151+
binary_path_matches(binary, candidate) if {
152+
candidate.ascii_case_folded != ""
153+
binary.ascii_case_folded_path == candidate.ascii_case_folded
154+
}
155+
156+
binary_glob_matches(binary, candidate) if {
157+
glob.match(binary.path, ["/"], candidate.path)
158+
}
159+
160+
binary_glob_matches(binary, candidate) if {
161+
candidate.ascii_case_folded != ""
162+
glob.match(binary.ascii_case_folded_path, ["/"], candidate.ascii_case_folded)
163+
}
164+
144165
# Binary matching: exact path.
145166
# SHA256 integrity is enforced in Rust via trust-on-first-use (TOFU) cache,
146167
# not in Rego. The proxy computes and caches binary hashes at runtime.
147168
binary_allowed(policy, exec) if {
148-
some b
149-
b := policy.binaries[_]
169+
some b in policy.binaries
150170
not contains(b.path, "*")
151171
b.path == exec.path
152172
}
153173

154174
# Binary matching: ancestor exact path (e.g., claude spawns node).
155175
binary_allowed(policy, exec) if {
156-
some b
157-
b := policy.binaries[_]
176+
some b in policy.binaries
158177
not contains(b.path, "*")
159-
ancestor := exec.ancestors[_]
178+
some ancestor in exec.ancestors
160179
b.path == ancestor
161180
}
162181

182+
# Trusted runtime evidence may additionally authorize an ASCII case-folded
183+
# Windows path. Legacy and synthetic L7 inputs omit match_paths and continue to
184+
# use only the exact rules above.
185+
binary_allowed(policy, exec) if {
186+
some b in policy.binaries
187+
not contains(b.path, "*")
188+
some candidate in exec.match_paths
189+
binary_path_matches(b, candidate)
190+
}
191+
163192
# Binary matching: glob pattern against exe path or any ancestor.
164193
# NOTE: cmdline_paths are intentionally excluded — argv[0] is trivially
165194
# spoofable via execve and must not be used as a grant-access signal.
166195
binary_allowed(policy, exec) if {
167196
some b in policy.binaries
168197
contains(b.path, "*")
169198
all_paths := array.concat([exec.path], exec.ancestors)
170-
some p in all_paths
171-
glob.match(b.path, ["/"], p)
199+
some path in all_paths
200+
glob.match(b.path, ["/"], path)
201+
}
202+
203+
binary_allowed(policy, exec) if {
204+
some b in policy.binaries
205+
contains(b.path, "*")
206+
some candidate in exec.match_paths
207+
binary_glob_matches(b, candidate)
172208
}
173209

174210
# --- Network action (allow / deny) ---

‎crates/openshell-supervisor-network/src/l7/relay.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2935,6 +2935,7 @@ fn evaluate_l7_request_once(
29352935
"path": ctx.binary_path,
29362936
"ancestors": ctx.ancestors,
29372937
"cmdline_paths": ctx.cmdline_paths,
2938+
"match_paths": engine.binary_match_paths(),
29382939
},
29392940
"request": {
29402941
"method": request.action,

0 commit comments

Comments
 (0)