authorgravatar for 71498452+StiensWout@users.noreply.github.comWout Stiens <71498452+StiensWout@users.noreply.github.com> 2026-06-22 19:15:43+02:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-06-22 10:15:43-07:00
log8919ae7c53973048827481ecc3a96d4d778955f7
treeb54e84c5ac2a9734f8c79ac649565835afdf288a
parentc1e2408e1f317bba2fbb48a27498b4ae4bef47d8
signature Signed by PGP key B5690EEEBB952194

[codex] Reject unsupported remote pairing protocols (#3498)

Co-authored-by: Codex <codex@openai.com>

2 files changed, 68 insertions(+), 2 deletions(-)

packages/shared/src/remote.test.ts+47
...@@ -72,6 +72,53 @@ describe("remote", () => {...@@ -72,6 +72,53 @@ describe("remote", () => {
72 });72 });
73 });73 });
7474
75 it("rejects unsupported direct pairing URL protocols", () => {
76 let pairingUrlError: unknown;
77 try {
78 resolveRemotePairingTarget({
79 pairingUrl: "ftp://remote.example.com/pair#token=pairing-token",
80 });
81 } catch (cause) {
82 pairingUrlError = cause;
83 }
84
85 expect(pairingUrlError).toBeInstanceOf(RemotePairingUrlInvalidError);
86 expect(pairingUrlError).toMatchObject({ protocol: "ftp:" });
87 expect((pairingUrlError as RemotePairingUrlInvalidError).cause).toBeUndefined();
88 });
89
90 it("rejects unsupported hosted pairing backend protocols", () => {
91 let hostError: unknown;
92 try {
93 resolveRemotePairingTarget({
94 pairingUrl:
95 "https://app.t3.codes/pair?host=ftp%3A%2F%2Fremote.example.com#token=pairing-token",
96 });
97 } catch (cause) {
98 hostError = cause;
99 }
100
101 expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError);
102 expect(hostError).toMatchObject({ source: "hosted-pairing-host", protocol: "ftp:" });
103 expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined();
104 });
105
106 it("rejects unsupported direct host protocols", () => {
107 let hostError: unknown;
108 try {
109 resolveRemotePairingTarget({
110 host: "ftp://remote.example.com",
111 pairingCode: "pairing-token",
112 });
113 } catch (cause) {
114 hostError = cause;
115 }
116
117 expect(hostError).toBeInstanceOf(RemoteBackendUrlInvalidError);
118 expect(hostError).toMatchObject({ source: "direct-host", protocol: "ftp:" });
119 expect((hostError as RemoteBackendUrlInvalidError).cause).toBeUndefined();
120 });
121
75 it("uses distinct structural errors for missing pairing inputs", () => {122 it("uses distinct structural errors for missing pairing inputs", () => {
76 expect(() => resolveRemotePairingTarget({})).toThrowError(RemoteBackendUrlMissingError);123 expect(() => resolveRemotePairingTarget({})).toThrowError(RemoteBackendUrlMissingError);
77 expect(() =>124 expect(() =>
packages/shared/src/remote.ts+21-2
...@@ -3,6 +3,7 @@ import * as Schema from "effect/Schema";...@@ -3,6 +3,7 @@ import * as Schema from "effect/Schema";
3const PAIRING_TOKEN_PARAM = "token";3const PAIRING_TOKEN_PARAM = "token";
4const HOSTED_PAIRING_HOST_PARAM = "host";4const HOSTED_PAIRING_HOST_PARAM = "host";
5const HOSTED_PAIRING_LABEL_PARAM = "label";5const HOSTED_PAIRING_LABEL_PARAM = "label";
6const SUPPORTED_REMOTE_BACKEND_PROTOCOLS = new Set(["http:", "https:", "ws:", "wss:"]);
67
7const readHashParams = (url: URL): URLSearchParams =>8const readHashParams = (url: URL): URLSearchParams =>
8 new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);9 new URLSearchParams(url.hash.startsWith("#") ? url.hash.slice(1) : url.hash);
...@@ -18,7 +19,10 @@ export class RemoteBackendUrlMissingError extends Schema.TaggedErrorClass<Remote...@@ -18,7 +19,10 @@ export class RemoteBackendUrlMissingError extends Schema.TaggedErrorClass<Remote
1819
19export class RemotePairingUrlInvalidError extends Schema.TaggedErrorClass<RemotePairingUrlInvalidError>()(20export class RemotePairingUrlInvalidError extends Schema.TaggedErrorClass<RemotePairingUrlInvalidError>()(
20 "RemotePairingUrlInvalidError",21 "RemotePairingUrlInvalidError",
21 { cause: Schema.Defect() },22 {
23 cause: Schema.optional(Schema.Defect()),
24 protocol: Schema.optional(Schema.String),
25 },
22) {26) {
23 override get message(): string {27 override get message(): string {
24 return "Pairing URL is invalid.";28 return "Pairing URL is invalid.";
...@@ -29,7 +33,8 @@ export class RemoteBackendUrlInvalidError extends Schema.TaggedErrorClass<Remote...@@ -29,7 +33,8 @@ export class RemoteBackendUrlInvalidError extends Schema.TaggedErrorClass<Remote
29 "RemoteBackendUrlInvalidError",33 "RemoteBackendUrlInvalidError",
30 {34 {
31 source: Schema.Literals(["direct-host", "hosted-pairing-host"]),35 source: Schema.Literals(["direct-host", "hosted-pairing-host"]),
32 cause: Schema.Defect(),36 cause: Schema.optional(Schema.Defect()),
37 protocol: Schema.optional(Schema.String),
33 },38 },
34) {39) {
35 override get message(): string {40 override get message(): string {
...@@ -64,6 +69,9 @@ export const RemotePairingTargetError = Schema.Union([...@@ -64,6 +69,9 @@ export const RemotePairingTargetError = Schema.Union([
64]);69]);
65export type RemotePairingTargetError = typeof RemotePairingTargetError.Type;70export type RemotePairingTargetError = typeof RemotePairingTargetError.Type;
6671
72const hasSupportedRemoteBackendProtocol = (url: URL): boolean =>
73 SUPPORTED_REMOTE_BACKEND_PROTOCOLS.has(url.protocol);
74
67const normalizeRemoteBaseUrl = (75const normalizeRemoteBaseUrl = (
68 rawValue: string,76 rawValue: string,
69 source: RemoteBackendUrlInvalidError["source"],77 source: RemoteBackendUrlInvalidError["source"],
...@@ -83,6 +91,12 @@ const normalizeRemoteBaseUrl = (...@@ -83,6 +91,12 @@ const normalizeRemoteBaseUrl = (
83 } catch (cause) {91 } catch (cause) {
84 throw new RemoteBackendUrlInvalidError({ source, cause });92 throw new RemoteBackendUrlInvalidError({ source, cause });
85 }93 }
94 if (!hasSupportedRemoteBackendProtocol(url)) {
95 throw new RemoteBackendUrlInvalidError({
96 source,
97 protocol: url.protocol,
98 });
99 }
86 url.pathname = "/";100 url.pathname = "/";
87 url.search = "";101 url.search = "";
88 url.hash = "";102 url.hash = "";
...@@ -184,6 +198,11 @@ export const resolveRemotePairingTarget = (input: {...@@ -184,6 +198,11 @@ export const resolveRemotePairingTarget = (input: {
184 } catch (cause) {198 } catch (cause) {
185 throw new RemotePairingUrlInvalidError({ cause });199 throw new RemotePairingUrlInvalidError({ cause });
186 }200 }
201 if (!hasSupportedRemoteBackendProtocol(url)) {
202 throw new RemotePairingUrlInvalidError({
203 protocol: url.protocol,
204 });
205 }
187 const hostedPairingRequest = readHostedPairingRequest(url);206 const hostedPairingRequest = readHostedPairingRequest(url);
188 if (hostedPairingRequest) {207 if (hostedPairingRequest) {
189 const hostedBackendUrl = normalizeRemoteBaseUrl(208 const hostedBackendUrl = normalizeRemoteBaseUrl(