Skip to content

fix(ocap-kernel): restrict plain ws:// relay dialing to private/allowed addresses#857

Open
rekmarks wants to merge 3 commits intomainfrom
rekm/restrict-ws-connections
Open

fix(ocap-kernel): restrict plain ws:// relay dialing to private/allowed addresses#857
rekmarks wants to merge 3 commits intomainfrom
rekm/restrict-ws-connections

Conversation

@rekmarks
Copy link
Member

@rekmarks rekmarks commented Feb 27, 2026

PR #855 switched webSockets() to use wsFilters.all to enable plain ws:// connections for private-network and reverse-proxy relay scenarios. As written, this also allowed ws:// to any public internet address, sending unencrypted traffic over the open internet.

This PR keeps the transport-level change (wsFilters.all must stay so the transport handles ws:// at all) and adds enforcement in connectionGater.denyDialMultiaddr.

Changes

  • types.ts: Adds allowedWsHosts?: string[] to both RemoteCommsOptions and ConnectionFactoryOptions — an explicit per-instance list of hostnames/IPs that are trusted for plain ws:// beyond the always-allowed private ranges.
  • transport.ts: Threads allowedWsHosts through to ConnectionFactory.make().
  • connection-factory.ts: Replaces the async () => false stub with real gating logic via two new module-level helpers:
    • isPlainWs(ma) — detects plain ws:// using ma.protoNames() (true only when ws is present and neither wss nor tls is).
    • isPrivateAddress(host) — checks IPv4 loopback (127.0.0.0/8), RFC 1918 ranges, IPv6 loopback/ULA/link-local by direct octet comparison (no bitwise operators).
    • denyDialMultiaddr allows wss://, WebRTC, and circuit-relay addresses unconditionally; allows ws:// only to private/loopback IPs or hosts on allowedWsHosts; denies everything else.
  • connection-factory.test.ts: Replaces the single stub test with a 9-case it.each suite covering secure transports, all four private IPv4 ranges, public IP denial, allowlisted/non-allowlisted hostnames, and non-WebSocket multiaddrs.

Testing

The new connectionGater.denyDialMultiaddr describe block exercises the gating function directly by passing minimal Multiaddr-shaped objects (with protoNames() and toOptions()) extracted from the createLibp2p mock call args. This avoids any libp2p networking and keeps the tests fast and deterministic.


Note

Medium Risk
Changes libp2p dialing policy for ws:// by denying public destinations unless explicitly allowlisted, which could break existing relay connectivity if deployments relied on public plain WebSocket relays. Logic is security-sensitive (transport encryption), but scope is contained to connection gating and option plumbing.

Overview
Prevents unencrypted ws:// dials to arbitrary public addresses by implementing connectionGater.denyDialMultiaddr in ConnectionFactory: non-plain-WebSocket multiaddrs (e.g. wss, WebRTC, circuit relay) are still allowed, while plain ws is only allowed for private/loopback IPs or allowedWsHosts.

Threads a new allowedWsHosts?: string[] option from RemoteCommsOptionsinitTransportConnectionFactoryOptions, and replaces the prior stubbed gater test with a parameterized suite covering private ranges, allowlisting, and public-deny cases.

Written by Cursor Bugbot for commit e896ead. This will update automatically on new commits. Configure here.

…ed addresses

Adds a connectionGater.denyDialMultiaddr implementation that blocks
plain ws:// connections to public internet addresses. Private/loopback
IPs (RFC 1918, 127.0.0.0/8, IPv6 ULA/link-local) and an explicit
per-instance allowedWsHosts list are always permitted; everything else
over plain ws:// is denied. wss://, WebRTC, and circuit-relay addresses
are unaffected.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rekmarks rekmarks requested a review from a team as a code owner February 27, 2026 01:35
@rekmarks rekmarks requested a review from sirtimid February 27, 2026 01:36
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Copy link

@cursor cursor bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.

… positives

The startsWith('fc'/'fd'/'fe80:') checks in isPrivateAddress could
match DNS hostnames like 'fcevil.attacker.com', allowing plain ws://
connections to attacker-controlled hosts. Fix by validating the host is
an actual IPv6 address using the URL parser (new URL('http://[<host>]')
throws for non-IPv6 strings) before applying the prefix checks. Adds a
regression test for the bypass.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@github-actions
Copy link
Contributor

Coverage Report

Status Category Percentage Covered / Total
🔵 Lines 76.05%
⬆️ +0.03%
6589 / 8663
🔵 Statements 75.95%
⬆️ +0.04%
6695 / 8815
🔵 Functions 73.87%
⬆️ +0.04%
1646 / 2228
🔵 Branches 75.35%
⬆️ +0.08%
2455 / 3258
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/ocap-kernel/src/remotes/types.ts 100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
100%
🟰 ±0%
packages/ocap-kernel/src/remotes/platform/connection-factory.ts 95.58%
⬇️ -1.69%
86.66%
⬇️ -3.34%
96.77%
⬆️ +0.48%
95.48%
⬇️ -1.74%
54, 74, 83, 341, 486, 520
packages/ocap-kernel/src/remotes/platform/transport.ts 82.37%
🟰 ±0%
80.72%
🟰 ±0%
74.19%
🟰 ±0%
82.37%
🟰 ±0%
117, 159-160, 165-169, 211-220, 253, 287-305, 329, 413, 457-460, 484, 508-513, 516-517, 521-524, 565, 595, 614-616, 625, 736
Generated in workflow #3833 for commit e896ead by the Vitest Coverage Report Action

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant