[Bubblewrap/SDK] Schema-0.8 network gate: an absent version disables it, and the Node SDK trips it on a field callers never set
- Dominant language
- Rust
- Stars
- 1.3k
- Forks
- 79
- Avg merge
- 2d 7h
- Merged PRs (30d)
- 117
Description
### Relevant area(s)
Linux (Bubblewrap backend), plus the Node SDK
### Brief description of your issue
PR #943 makes an unenforceable Bubblewrap network policy a hard error on schema
0.8+ (`enforcementMode: firewall`/`both`, and an `allowLocalNetwork` the
namespace cannot honor). Two entry points into that gate are wrong in opposite
directions. They are filed together because both are about *which requests
reach the gate*, not about the gate's own logic.
**1. An absent `version` silently disables the gate.**
`schema_enforces_network_strictly` treats an unparsable version as pre-0.8
(`src/core/wxc_common/src/config_parser.rs:300-303`), and `version` is optional
at every layer: the parser defaults it to `""`
(`config_parser.rs:797`, `cfg.version.unwrap_or_default()`), `validate_schema_version`
returns `Ok` for an empty string (`config_parser.rs:308-312`), the dev schema
declares no top-level `required`, and `docs/versioning.md:368-372` documents
"in range / **absent** → continue". The existing test
`schema_version_absent_accepted` pins this.
So a config that simply omits `version` gets the pre-0.8 lenient path and keeps
the silently-unenforced firewall behavior. This is not a privilege bypass — the
config author is the policy author — but it does mean the fail-closed behavior
depends on a field nothing requires. Omission currently resolves to the
*oldest* semantics rather than the newest.
**2. The Node SDK sets `enforcementMode: 'firewall'` on the caller's behalf,
so Bubblewrap callers hit the gate for a field they never wrote.**
`applyLinuxNetworkPolicy` promotes the mode whenever host rules are present
without a proxy:
`sdk/node/src/helper.ts:135-139`
```ts
const hasHostRules =
!!(config.network.allowedHosts?.length || config.network.blockedHosts?.length);
if (hasHostRules && !hasProxy) {
config.network.enforcementMode = 'firewall';
}
```
That helper is explicitly shared with the Bubblewrap builder (`helper.ts:109-111`),
and `sdk/node/src/sandbox.ts:14` accepts `0.8.0-alpha`. A caller who sets only
`allowedHosts` on Bubblewrap at 0.8 therefore gets a rejection naming
`network.enforcementMode`, which does not appear anywhere in their policy.
The rejection itself is correct — that caller is silently unenforced today,
which is the bug #943 closes. What is wrong is that the SDK auto-selects an
enforcement path Bubblewrap cannot provide instead of routing the caller to the
one it can (`network.proxy`), and then surfaces an error about a field the SDK
authored.
### Steps to reproduce
Bypass (1), on Linux with the Bubblewrap backend:
1. Take any config that sets `network.enforcementMode: "firewall"` with
`allowedHosts`, e.g. `tests/configs/bubblewrap_network_firewall_rejected.json`.
2. Delete its `"version"` field.
3. Run it: `lxc-exec --experimental .json`.
Breakage (2), via the Node SDK on Linux:
1. Build a `SandboxPolicy` with `version: '0.8.0-alpha'`, `containment:
'bubblewrap'`, and `network.allowedHosts: ['api.github.com']`. Set no proxy
and no `enforcementMode`.
2. Call `spawnSandbox`.
### Expected behavior
1. Omitting `version` should not select the more permissive of two security
postures. Either treat an absent version as the current schema (fail closed),
or reject an absent version specifically when a policy requests one of these
unenforceable controls.
2. The SDK should not promote Bubblewrap to an enforcement mode the backend
cannot honor. It should either leave the mode unset and report that host
filtering on Bubblewrap requires `network.proxy`, or promote only for LXC,
which has the veth and privilege the firewall path needs.
### Actual behavior
1. The run is accepted on the pre-0.8 path. `enforcementMode: "firewall"` builds
an iptables chain that is never hooked into `FORWARD` (see #755, folded into
#896), the run exits 0, and no traffic is filtered.
2. The request is rejected at parse time with
`Bubblewrap: network.enforcementMode='firewall' and 'both' are not supported...`,
naming a field the caller never set.
---
Related: #901 (SDK surfaces disagreeing with engine behavior) — (2) is the same
family but is a behavioral auto-promotion rather than a type mismatch.
#755 / #896 for the underlying unattached-chain fail-open that #943 closes.
Contributor guide
Assessment
This issue has not been assessed yet.