From f62aec1a030df929f4827fb2dde5b4cf6bda50d2 Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 13 Jul 2026 15:39:41 +0200 Subject: [PATCH] fix(hub): bind socket transport to Hub authority --- .../src/server/hub/relationship-remote.test.ts | 18 ++++++++++++++++++ .../src/server/hub/relationship-remote.ts | 13 ++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/packages/server/src/server/hub/relationship-remote.test.ts b/packages/server/src/server/hub/relationship-remote.test.ts index d95f46f70d..280f6a7841 100644 --- a/packages/server/src/server/hub/relationship-remote.test.ts +++ b/packages/server/src/server/hub/relationship-remote.test.ts @@ -91,6 +91,24 @@ test("enrollment rejects a transport URL that cannot open a WebSocket", async () ).rejects.toThrow("Hub WebSocket URL must use ws or wss"); }); +test("enrollment rejects a WebSocket outside the enrolled Hub authority", async () => { + const hubOrigin = await startEnrollmentHub("ws://other-hub.test/daemon"); + const remote = new DirectHubRelationshipRemote(); + + await expect( + remote.enroll({ + relationshipId: "relationship-1", + idempotencyKey: "ceremony-1", + hubOrigin, + token: "token", + serverId: "server-1", + daemonPublicKey: "public-key", + credentialVerifier: "verifier", + scopes: ["hub.*"], + }), + ).rejects.toThrow("Hub WebSocket URL must match the Hub origin"); +}); + test.each(["enrollment", "revocation"])("%s HTTP calls are bounded", async (operation) => { const hubOrigin = await startStalledHub(); const remote = new DirectHubRelationshipRemote({ requestTimeoutMs: 25 }); diff --git a/packages/server/src/server/hub/relationship-remote.ts b/packages/server/src/server/hub/relationship-remote.ts index 1408f7e2f7..37e1fbb95e 100644 --- a/packages/server/src/server/hub/relationship-remote.ts +++ b/packages/server/src/server/hub/relationship-remote.ts @@ -66,6 +66,15 @@ const EnrollmentResultSchema = z.object({ }), }); +function ensureWebSocketMatchesHubOrigin(hubOrigin: string, webSocketUrl: string): void { + const hub = new URL(hubOrigin); + const socket = new URL(webSocketUrl); + const expectedProtocol = hub.protocol === "https:" ? "wss:" : "ws:"; + if (socket.protocol !== expectedProtocol || socket.host !== hub.host) { + throw new Error("Hub WebSocket URL must match the Hub origin"); + } +} + export class DirectHubRelationshipRemote implements HubRelationshipRemote { private readonly requestTimeoutMs: number; @@ -97,7 +106,9 @@ export class DirectHubRelationshipRemote implements HubRelationshipRemote { } throw new Error(`Hub enrollment failed (${response.status})`); } - return EnrollmentResultSchema.parse(await response.json()); + const enrollment = EnrollmentResultSchema.parse(await response.json()); + ensureWebSocketMatchesHubOrigin(input.hubOrigin, enrollment.webSocketUrl); + return enrollment; }); }