From e220a51eab5db3d1bc2c8c7391e234d3842e4ce5 Mon Sep 17 00:00:00 2001 From: Dotta Date: Sat, 5 Sep 2026 08:10:26 -0500 Subject: [PATCH] fix(connectors): allowlist provider authorization endpoints --- .../paperclip-cloud-connector.test.ts | 24 +++++++++++++++++++ .../src/services/paperclip-cloud-connector.ts | 11 +++++++++ 2 files changed, 35 insertions(+) diff --git a/server/src/services/paperclip-cloud-connector.test.ts b/server/src/services/paperclip-cloud-connector.test.ts index a0319b1264..1f52c38ad9 100644 --- a/server/src/services/paperclip-cloud-connector.test.ts +++ b/server/src/services/paperclip-cloud-connector.test.ts @@ -115,11 +115,35 @@ describe("Paperclip Cloud connector", () => { }); }); + it("accepts the fixed Google authorization endpoint for Google profiles", async () => { + const keys = config(); + const connector = createPaperclipCloudConnector({ + config: keys.config, + request: vi.fn(async () => Response.json({ + confirmationUrl: "https://my.example.test/connections/confirm?session=broker-state", + authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth?client_id=client&state=broker-state", + expiresAt: "2099-08-21T20:00:00.000Z", + })) as typeof fetch, + }); + + await expect(connector.startAuthorization({ + subject, + companyId, + profile: "gmail.draft", + returnUri: "https://paperclip.example.test/api/tools/oauth/cloud-connector/callback", + returnState: "state-direct-google", + })).resolves.toMatchObject({ + authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth?client_id=client&state=broker-state", + }); + }); + it.each([ ["non-string", { href: "https://github.com/login/oauth/authorize" }], ["plaintext HTTP", "http://github.com/login/oauth/authorize"], ["embedded credentials", "https://user:password@github.com/login/oauth/authorize"], ["fragment", "https://github.com/login/oauth/authorize#unexpected"], + ["unapproved HTTPS origin", "https://attacker.example.test/login/oauth/authorize"], + ["unapproved provider path", "https://github.com/session/authorize"], ["not a URL", "not-a-url"], ])("rejects a malformed direct provider URL: %s", async (_label, authorizationUrl) => { const keys = config(); diff --git a/server/src/services/paperclip-cloud-connector.ts b/server/src/services/paperclip-cloud-connector.ts index 1932142414..d03f7011bc 100644 --- a/server/src/services/paperclip-cloud-connector.ts +++ b/server/src/services/paperclip-cloud-connector.ts @@ -380,6 +380,7 @@ export function createPaperclipCloudConnector(input: { || authorizationUrl.username || authorizationUrl.password || authorizationUrl.hash + || !isExpectedProviderAuthorizationUrl(profile, authorizationUrl) ) { throw new PaperclipCloudConnectorError("Paperclip Cloud connector returned an invalid provider URL", "CONNECTOR_BAD_RESPONSE"); } @@ -678,6 +679,16 @@ function connectorProfileDefinition(profile: PaperclipCloudConnectorProfileId): return { provider: "google", scopes: GOOGLE_WORKSPACE_CONNECTOR_PROFILES[profile].scopes }; } +function isExpectedProviderAuthorizationUrl( + profile: PaperclipCloudConnectorProfileId, + url: URL, +): boolean { + if (isGitHubConnectorProfileId(profile)) { + return url.origin === "https://github.com" && url.pathname === "/login/oauth/authorize"; + } + return url.origin === "https://accounts.google.com" && url.pathname === "/o/oauth2/v2/auth"; +} + function isPaperclipCloudConnectorProfileId(value: string): value is PaperclipCloudConnectorProfileId { return isGoogleWorkspaceConnectorProfileId(value) || isGitHubConnectorProfileId(value); }