microsoft / microsoft/mxc

[Bubblewrap/SDK] Schema-0.8 network gate: an absent version disables it, and the Node SDK trips it on a field callers never set

Open
#945 2 comments 0 reactions 1 assignee Assigned to @SohamDas2021 View on GitHub
Area-Executor-Schema Area-SDK-Api Area-SDK-Configuration Issue-Bug OS-Linux Priority3
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.