diff --git a/crates/kernel/tests/permissions.rs b/crates/kernel/tests/permissions.rs index 6876bcaaac..48283072da 100644 --- a/crates/kernel/tests/permissions.rs +++ b/crates/kernel/tests/permissions.rs @@ -536,6 +536,37 @@ fn permission_glob_double_star_still_matches_nested_paths() { assert!(permission_glob_matches("tcp://**", "tcp://127.0.0.1:43111")); } +#[test] +fn network_permission_patterns_match_the_resource_uri_not_a_bare_host() { + // Network resources are formatted as URIs (`format_tcp_resource` / + // `format_dns_resource`), so a pattern is matched against the whole URI. + assert!(permission_glob_matches( + "tcp://api.example.com:443", + "tcp://api.example.com:443", + )); + assert!(permission_glob_matches( + "tcp://api.example.com:*", + "tcp://api.example.com:443", + )); + assert!(permission_glob_matches( + "dns://api.example.com", + "dns://api.example.com", + )); + + // A bare host or `host:port` never matches, and a single `*` cannot reach + // past the `//`, so neither form can be used to allow or deny a host. + assert!(!permission_glob_matches( + "api.example.com", + "tcp://api.example.com:443", + )); + assert!(!permission_glob_matches( + "api.example.com:443", + "tcp://api.example.com:443", + )); + assert!(!permission_glob_matches("*", "tcp://api.example.com:443")); + assert!(!permission_glob_matches("*.example.com", "dns://api.example.com")); +} + #[test] fn kernel_vm_config_defaults_to_deny_all_permissions() { let mut kernel = KernelVm::new(MemoryFileSystem::new(), KernelVmConfig::new("vm-defaults")); diff --git a/docs/content/docs/permissions.mdx b/docs/content/docs/permissions.mdx index 5adeec23d4..481538fb7c 100644 --- a/docs/content/docs/permissions.mdx +++ b/docs/content/docs/permissions.mdx @@ -77,7 +77,9 @@ To invert it, flip `default` to `"deny"` and allow just one subtree: ## Allow only specific network hosts -Every non-`fs` scope matches by `patterns` instead of `paths`. For `network`, a pattern is a host (or `host:port`), and the operations are `fetch`, `http`, `dns`, and `listen`. +Every non-`fs` scope matches by `patterns` instead of `paths`. The operations for `network` are `fetch`, `http`, `dns`, and `listen`. + +A `network` pattern is matched against the URI form of the connection, not a bare host: `tcp://:` for `fetch`, `http`, and `listen`, and `dns://` for `dns`. Patterns are globs in which a single `*` does not cross `/`, so use `tcp://api.example.com:*` to allow every port on a host, and `tcp://**` to allow every host. A bare `api.example.com` matches nothing. diff --git a/examples/permissions/server.ts b/examples/permissions/server.ts index f429690da0..df52018425 100644 --- a/examples/permissions/server.ts +++ b/examples/permissions/server.ts @@ -15,11 +15,18 @@ const denyVault = { } satisfies Permissions; // docs:start allow-one-host -// Deny the network by default, allow only api.example.com. +// Deny the network by default, allow only api.example.com (any port). +// Patterns match the URI form of the connection, so a bare host never matches. const allowOneHost = { network: { default: "deny", - rules: [{ mode: "allow", operations: ["*"], patterns: ["api.example.com"] }], + rules: [ + { + mode: "allow", + operations: ["*"], + patterns: ["tcp://api.example.com:*", "dns://api.example.com"], + }, + ], }, } satisfies Permissions; // docs:end allow-one-host