Skip to content

Capability escape: omitting [capabilities] from module.toml grants unrestricted host-import access #60

Description

@lgahdl

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.rsenforce_capabilities()
  • crates/nexum-runtime/src/manifest/load.rsload() / 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions