diff --git a/.changeset/twelve-snails-wait.md b/.changeset/twelve-snails-wait.md new file mode 100644 index 0000000000..02bf3f77f6 --- /dev/null +++ b/.changeset/twelve-snails-wait.md @@ -0,0 +1,5 @@ +--- +"react-router": patch +--- + +Sync protocol validation to rsc flows diff --git a/packages/react-router/lib/router/router.ts b/packages/react-router/lib/router/router.ts index dfcbb108bf..e383f06662 100644 --- a/packages/react-router/lib/router/router.ts +++ b/packages/react-router/lib/router/router.ts @@ -6501,28 +6501,28 @@ function normalizeRelativeRoutingRedirectResponse( return response; } +// Match Chrome's behavior: +// https://github.com/chromium/chromium/blob/216dbeb61db0c667e62082e5f5400a32d6983df3/content/public/common/url_utils.cc#L82 +export const invalidProtocols = [ + "about:", + "blob:", + "chrome:", + "chrome-untrusted:", + "content:", + "data:", + "devtools:", + "file:", + "filesystem:", + // eslint-disable-next-line no-script-url + "javascript:", +]; + function normalizeRedirectLocation( location: string, currentUrl: URL, basename: string, historyInstance: History, ): string { - // Match Chrome's behavior: - // https://github.com/chromium/chromium/blob/216dbeb61db0c667e62082e5f5400a32d6983df3/content/public/common/url_utils.cc#L82 - let invalidProtocols = [ - "about:", - "blob:", - "chrome:", - "chrome-untrusted:", - "content:", - "data:", - "devtools:", - "file:", - "filesystem:", - // eslint-disable-next-line no-script-url - "javascript:", - ]; - if (isAbsoluteUrl(location)) { // Strip off the protocol+origin for same-origin + same-basename absolute redirects let normalizedLocation = location; diff --git a/packages/react-router/lib/rsc/browser.tsx b/packages/react-router/lib/rsc/browser.tsx index b32c9f3f15..4e557b0b09 100644 --- a/packages/react-router/lib/rsc/browser.tsx +++ b/packages/react-router/lib/rsc/browser.tsx @@ -7,7 +7,11 @@ import { FrameworkContext, setIsHydrated } from "../dom/ssr/components"; import type { FrameworkContextObject } from "../dom/ssr/entry"; import { createBrowserHistory, invariant } from "../router/history"; import type { Router as DataRouter, RouterInit } from "../router/router"; -import { createRouter, isMutationMethod } from "../router/router"; +import { + createRouter, + invalidProtocols, + isMutationMethod, +} from "../router/router"; import type { RSCPayload, RSCRouteManifest, @@ -140,6 +144,9 @@ export function createCallServer({ .then(async (payload) => { if (payload.type === "redirect") { if (payload.reload || isExternalLocation(payload.location)) { + if (hasInvalidProtocol(payload.location)) { + throw new Error("Invalid redirect location"); + } window.location.href = payload.location; return; } @@ -164,6 +171,9 @@ export function createCallServer({ ) { if (rerender.type === "redirect") { if (rerender.reload || isExternalLocation(rerender.location)) { + if (hasInvalidProtocol(rerender.location)) { + throw new Error("Invalid redirect location"); + } window.location.href = rerender.location; return; } @@ -1082,6 +1092,14 @@ function isExternalLocation(location: string) { return newLocation.origin !== window.location.origin; } +function hasInvalidProtocol(location: string): boolean { + try { + return invalidProtocols.includes(new URL(location).protocol); + } catch { + return false; + } +} + function cloneRoutes(routes: DataRouteObject[] | undefined): DataRouteObject[] { if (!routes) return undefined as any; return routes.map((route) => ({