fix(ocap-kernel): restrict plain ws:// relay dialing to private/allowed addresses#857
Open
fix(ocap-kernel): restrict plain ws:// relay dialing to private/allowed addresses#857
Conversation
…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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… 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>
Contributor
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||||||||
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2c812fe to
cf49c01
Compare
There was a problem hiding this comment.
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.
…ostname bypass
An all-hex hostname like 'fdcafe' passed the previous /^[0-9a-f:]+$/
regex and the startsWith('fd') check, causing isPrivateAddress to return
true and allowing plain ws:// to attacker-controlled hosts. Every valid
IPv6 address contains at least one colon; requiring one in the regex
closes the gap. Adds a regression test for the bypass.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
PR #855 switched
webSockets()to usewsFilters.allto enable plainws://connections for private-network and reverse-proxy relay scenarios. As written, this also allowedws://to any public internet address, sending unencrypted traffic over the open internet.This PR keeps the transport-level change (
wsFilters.allmust stay so the transport handlesws://at all) and adds enforcement inconnectionGater.denyDialMultiaddr.Changes
types.ts: AddsallowedWsHosts?: string[]to bothRemoteCommsOptionsandConnectionFactoryOptions— an explicit per-instance list of hostnames/IPs that are trusted for plainws://beyond the always-allowed private ranges.transport.ts: ThreadsallowedWsHoststhrough toConnectionFactory.make().connection-factory.ts: Replaces theasync () => falsestub with real gating logic via two new module-level helpers:isPlainWs(ma)— detects plainws://usingma.protoNames()(true only whenwsis present and neitherwssnortlsis).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).denyDialMultiaddrallowswss://, WebRTC, and circuit-relay addresses unconditionally; allowsws://only to private/loopback IPs or hosts onallowedWsHosts; denies everything else.connection-factory.test.ts: Replaces the single stub test with a 9-caseit.eachsuite covering secure transports, all four private IPv4 ranges, public IP denial, allowlisted/non-allowlisted hostnames, and non-WebSocket multiaddrs.Testing
The new
connectionGater.denyDialMultiaddrdescribe block exercises the gating function directly by passing minimalMultiaddr-shaped objects (withprotoNames()andtoOptions()) extracted from thecreateLibp2pmock call args. This avoids any libp2p networking and keeps the tests fast and deterministic.Note
Medium Risk
Changes libp2p connection gating for
ws://dials, which can impact connectivity if deployments rely on public plaintext WebSocket relays without configuringallowedWsHosts. Logic is security-sensitive (transport encryption policy) but narrowly scoped and covered by new table-driven tests.Overview
Tightens relay dialing security by adding
connectionGater.denyDialMultiaddrlogic that denies plaintextws://connections to public/unrecognised hosts while still allowingwss://and non-WebSocket transports.Introduces a new
allowedWsHostsoption (plumbed fromRemoteCommsOptionsthroughtransport.tsintoConnectionFactory) to permit specific non-privatews://relay hosts when needed, and replaces the prior “allow all” gater stub with RFC-based private/loopback detection plus an allowlist.Updates
connection-factory.test.tsto validate the new gating behavior across private IPv4 ranges, public IP denial, allowlisted hostnames, and non-WebSocket multiaddrs.Written by Cursor Bugbot for commit 0b7674f. This will update automatically on new commits. Configure here.