From ddea4aea1e14969145c6b2ca57f119e844fd3853 Mon Sep 17 00:00:00 2001 From: Andrew Jiang Date: Tue, 7 Jan 2025 15:51:55 -0500 Subject: [PATCH] fix: redirect allowlist for authed previews (#1958) --- .vscode/settings.json | 20 +++--- .../bundle/src/server/FernNextResponse.ts | 6 +- .../src/server/auth/allowed-redirects.ts | 68 ++++++++++++++++--- .../bundle/src/server/auth/getAuthState.ts | 16 +++-- 4 files changed, 86 insertions(+), 24 deletions(-) diff --git a/.vscode/settings.json b/.vscode/settings.json index 60ab6a0b84..218f4b8234 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -1,20 +1,22 @@ { "search.exclude": { ".vscode": true, - "pnpm-lock.yaml": true, + "**/.next": true, "**/.pnp.*": true, + "**/.turbo": true, "**/.yarn": true, - "**/node_modules": true, - "**/dist": true, + "**/__snapshots__/**": true, + "**/__test__/**/fixtures": true, + "**/__test__/**/output": true, "**/build": true, - "**/out": true, + "**/dist": true, + "**/generated": true, "**/lib": true, - "**/.next": true, + "**/node_modules": true, + "**/out": true, "**/storybook-static": true, - "**/generated": true, - "**/__test__/**/fixtures": true, - "**/__test__/**/output": true, - "**/__snapshots__/**": true, + "**/*.tsbuildinfo": true, + "pnpm-lock.yaml": true, "tests": true }, "typescript.enablePromptUseWorkspaceTsdk": true, diff --git a/packages/fern-docs/bundle/src/server/FernNextResponse.ts b/packages/fern-docs/bundle/src/server/FernNextResponse.ts index 3d7c24b936..cf52b940d8 100644 --- a/packages/fern-docs/bundle/src/server/FernNextResponse.ts +++ b/packages/fern-docs/bundle/src/server/FernNextResponse.ts @@ -27,8 +27,10 @@ export class FernNextResponse { !allowedDomains.includes(redirectLocation.host) && !isBuildWithFern(redirectLocation.host) ) { - // open redirect to unknown host detected: - return new NextResponse(null, { status: 410 }); + console.error( + `Redirect to ${redirectLocation.host} is not allowed. Allowed domains: ${allowedDomains.join(", ")}` + ); + return new NextResponse(null, { status: 403 }); } return NextResponse.redirect(redirectLocation, init); diff --git a/packages/fern-docs/bundle/src/server/auth/allowed-redirects.ts b/packages/fern-docs/bundle/src/server/auth/allowed-redirects.ts index 73de4f7604..406f3e9695 100644 --- a/packages/fern-docs/bundle/src/server/auth/allowed-redirects.ts +++ b/packages/fern-docs/bundle/src/server/auth/allowed-redirects.ts @@ -1,23 +1,75 @@ -import { AuthEdgeConfig } from "@fern-docs/auth"; +/** + * In order to prevent open-redirection, we need to curate a list of allowed domains where the server can redirect to. + */ + +import { AuthEdgeConfig, OAuth2, SSOWorkOS } from "@fern-docs/auth"; +import { PreviewUrlAuth } from "@fern-docs/edge-config"; import { compact } from "es-toolkit/array"; +import { UnreachableCaseError } from "ts-essentials"; const WORKOS_API_URL = "https://api.workos.com"; +const WEBFLOW_API_URL = "https://webflow.com"; export function getAllowedRedirectUrls( - authConfig?: AuthEdgeConfig | undefined + authConfig?: AuthEdgeConfig | undefined, + previewAuthConfig?: PreviewUrlAuth | undefined ): string[] { + return [ + ...getAllowedRedirectUrlsForAuthConfig(authConfig), + ...getAllowedRedirectUrlsForPreviewAuthConfig(previewAuthConfig), + ]; +} + +function getAllowedRedirectUrlsForAuthConfig(authConfig?: AuthEdgeConfig) { if (authConfig == null) { return []; } - if (authConfig.type === "basic_token_verification") { - return compact([authConfig.redirect, authConfig.logout]); + switch (authConfig.type) { + case "basic_token_verification": + // since the `redirect` and `logout` are configured in the edge config, we can trust them + return compact([authConfig.redirect, authConfig.logout]); + case "sso": + return getAllowedRedirectUrlsForSSO(authConfig); + case "oauth2": + return getAllowedRedirectUrlsForOAuth2(authConfig); + default: + console.error(new UnreachableCaseError(authConfig)); + } + + return []; +} + +function getAllowedRedirectUrlsForPreviewAuthConfig( + previewAuthConfig?: PreviewUrlAuth +) { + if (previewAuthConfig == null) { + return []; + } + + switch (previewAuthConfig.type) { + case "workos": + return [WORKOS_API_URL]; + default: + console.error(new UnreachableCaseError(previewAuthConfig.type)); } - if (authConfig.type === "sso") { - if (authConfig.partner === "workos") { - return compact([WORKOS_API_URL]); - } + return []; +} + +function getAllowedRedirectUrlsForSSO(_authConfig: SSOWorkOS) { + return [WORKOS_API_URL]; +} + +function getAllowedRedirectUrlsForOAuth2(authConfig: OAuth2) { + switch (authConfig.partner) { + case "ory": + // since the environment is configured in the edge config, we can trust it + return [authConfig.environment]; + case "webflow": + return [WEBFLOW_API_URL]; + default: + console.error(new UnreachableCaseError(authConfig)); } return []; diff --git a/packages/fern-docs/bundle/src/server/auth/getAuthState.ts b/packages/fern-docs/bundle/src/server/auth/getAuthState.ts index fb762d3de1..102fbf1852 100644 --- a/packages/fern-docs/bundle/src/server/auth/getAuthState.ts +++ b/packages/fern-docs/bundle/src/server/auth/getAuthState.ts @@ -175,6 +175,10 @@ export async function getAuthState( ): Promise { authConfig ??= await getAuthEdgeConfig(domain); const orgMetadata = await getOrgMetadataForDomain(withoutStaging(domain)); + const previewAuthConfig = + orgMetadata != null + ? await getPreviewUrlAuthConfig(orgMetadata) + : undefined; const authState = await getAuthStateInternal({ host, @@ -182,17 +186,19 @@ export async function getAuthState( pathname, authConfig, setFernToken, - previewAuthConfig: - orgMetadata != null - ? await getPreviewUrlAuthConfig(orgMetadata) - : undefined, + previewAuthConfig, }); + const allowedDestinations = getAllowedRedirectUrls( + authConfig, + previewAuthConfig + ); + return { ...authState, domain, host, - allowedDestinations: getAllowedRedirectUrls(authConfig), + allowedDestinations, }; }