Summary
A WASM module's module.toml capability declarations ([capabilities] with required/optional lists) can be bypassed entirely by omitting the [capabilities] section. When [capabilities] is absent, enforce_capabilities() falls back to a "0.1 compatibility" mode that grants access to every registry-mapped capability (chain, local-store, remote-store, messaging) regardless of what the module actually declares.
Affected code
crates/nexum-runtime/src/manifest/capabilities.rs — enforce_capabilities()
crates/nexum-runtime/src/manifest/load.rs — load() / fallback_manifest()
Finding
enforce_capabilities() computes let fallback = caps.is_none(); — true whenever module.toml has no [capabilities] table at all. In fallback mode, the per-import gating loop skips the registry-mapped capability check for chain, local-store, remote-store, and messaging entirely — any of these host imports the module's compiled component references get linked in, with no declaration required.
load() does emit a tracing::warn! when this happens ("no [capabilities] section in module.toml - defaulting to all-required (0.1 behaviour). This default will be removed in 0.3..."), so it's not silent — but it's a warning, not a boot failure, and nothing stops a manifest shipping this way in production.
HTTP is not affected: wasi:http capability gating is enforced by a separate, independently fail-closed allowlist mechanism (HttpGate/host_allowed()), so an empty or missing [capabilities.http].allow still denies all outbound HTTP regardless of this fallback.
A control test with an explicit-but-empty [capabilities] block (as opposed to omitting the section) behaves correctly — the same import is rejected.
Impact
A module author who forgets (or a manifest that's hand-edited/corrupted and loses) the [capabilities] section gets silently upgraded to full 0.1-style access to chain/local-store/remote-store/messaging, without the manifest's required/optional lists being consulted at all. A malicious module developer could deliberately omit the section to reach host imports never declared to an operator who only reviews the required/optional lists.
Suggested fix
Treat an absent [capabilities] section as "no capabilities granted" (empty-by-default) rather than "all capabilities granted" (0.1 legacy fallback), ahead of the already-planned 0.3 removal — or at minimum, make the missing-section case a hard boot error rather than a warning, so it can't reach production silently.
Found via
Internal red-team exercise (malicious module developer persona). Verified with a dynamic test: a module declaring only ["logging"] in required, with [capabilities] omitted entirely, was still able to link and call chain.request at runtime; a control test with an explicit empty [capabilities] block correctly rejects the same import.
Summary
A WASM module's
module.tomlcapability declarations ([capabilities]withrequired/optionallists) can be bypassed entirely by omitting the[capabilities]section. When[capabilities]is absent,enforce_capabilities()falls back to a "0.1 compatibility" mode that grants access to every registry-mapped capability (chain,local-store,remote-store,messaging) regardless of what the module actually declares.Affected code
crates/nexum-runtime/src/manifest/capabilities.rs—enforce_capabilities()crates/nexum-runtime/src/manifest/load.rs—load()/fallback_manifest()Finding
enforce_capabilities()computeslet fallback = caps.is_none();— true whenevermodule.tomlhas no[capabilities]table at all. In fallback mode, the per-import gating loop skips the registry-mapped capability check forchain,local-store,remote-store, andmessagingentirely — any of these host imports the module's compiled component references get linked in, with no declaration required.load()does emit atracing::warn!when this happens ("no [capabilities] section in module.toml - defaulting to all-required (0.1 behaviour). This default will be removed in 0.3..."), so it's not silent — but it's a warning, not a boot failure, and nothing stops a manifest shipping this way in production.HTTP is not affected:
wasi:httpcapability gating is enforced by a separate, independently fail-closed allowlist mechanism (HttpGate/host_allowed()), so an empty or missing[capabilities.http].allowstill denies all outbound HTTP regardless of this fallback.A control test with an explicit-but-empty
[capabilities]block (as opposed to omitting the section) behaves correctly — the same import is rejected.Impact
A module author who forgets (or a manifest that's hand-edited/corrupted and loses) the
[capabilities]section gets silently upgraded to full 0.1-style access to chain/local-store/remote-store/messaging, without the manifest'srequired/optionallists being consulted at all. A malicious module developer could deliberately omit the section to reach host imports never declared to an operator who only reviews therequired/optionallists.Suggested fix
Treat an absent
[capabilities]section as "no capabilities granted" (empty-by-default) rather than "all capabilities granted" (0.1 legacy fallback), ahead of the already-planned 0.3 removal — or at minimum, make the missing-section case a hard boot error rather than a warning, so it can't reach production silently.Found via
Internal red-team exercise (
malicious module developerpersona). Verified with a dynamic test: a module declaring only["logging"]inrequired, with[capabilities]omitted entirely, was still able to link and callchain.requestat runtime; a control test with an explicit empty[capabilities]block correctly rejects the same import.