Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/twelve-snails-wait.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"react-router": patch
---

Sync protocol validation to rsc flows
32 changes: 16 additions & 16 deletions packages/react-router/lib/router/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
20 changes: 19 additions & 1 deletion packages/react-router/lib/rsc/browser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}
Expand All @@ -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;
}
Expand Down Expand Up @@ -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) => ({
Expand Down