mirror of
https://github.com/cloudflare/vinext.git
synced 2026-05-09 08:25:34 +02:00
[PR #906] [MERGED] fix(app-router): block javascript: URLs in router.push/replace/prefetch #938
Labels
No labels
enhancement
enhancement
good first issue
help wanted
nextjs-tracking
nextjs-tracking
pull-request
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
starred/vinext#938
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
📋 Pull Request Information
Original PR: https://github.com/cloudflare/vinext/pull/906
Author: @NathanDrake2406
Created: 4/26/2026
Status: ✅ Merged
Merged: 4/26/2026
Merged by: @james-elicx
Base:
main← Head:nathan/router-block-javascript-uri📝 Commits (4)
fb819fafix(app-router): block javascript: URLs in router.push/replace/prefetch1ed1b48test(router): split ported vs vinext-only dangerous-scheme cases5cb5af3fix app router dangerous redirect gaps520e8betest address dangerous URL review nits📊 Changes
12 files changed (+295 additions, -5 deletions)
View changed files
📝
packages/vinext/src/server/app-browser-entry.ts(+6 -0)📝
packages/vinext/src/shims/navigation.ts(+4 -0)📝
packages/vinext/src/shims/server.ts(+17 -4)📝
packages/vinext/src/shims/url-safety.ts(+9 -0)➕
tests/e2e/app-router/nextjs-compat/javascript-urls.spec.ts(+98 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/javascript-urls/action-redirect-form/page.tsx(+22 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/javascript-urls/action-redirect-onclick/page.tsx(+20 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/javascript-urls/bad-url.ts(+2 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/javascript-urls/boom/page.tsx(+3 -0)➕
tests/fixtures/app-basic/app/nextjs-compat/javascript-urls/safe/page.tsx(+3 -0)➕
tests/router-javascript-urls.test.ts(+93 -0)📝
tests/shims.test.ts(+18 -1)📄 Description
What this changes
This PR blocks dangerous navigation URLs across the App Router surfaces that can reach browser navigation sinks:
useRouter().push,.replace, and.prefetchreject dangerous URI schemes.redirect("javascript:...")responses are blocked before the browser entry assigns towindow.location.*.NextResponse.redirect()andNextResponse.rewrite()now validate URLs before writingLocation/x-middleware-rewriteheaders.The block message intentionally matches Next.js exactly:
Next.js has blocked a javascript: URL as a security precaution.Why
router.push("javascript:alert(1)")previously flowed intonavigateClientSide, was classified as external, and reachedwindow.location.assign, which browsers treat as script execution.After the initial router fix, two related paths were still open:
x-action-redirect; the browser entry consumed that header and sent it directly towindow.location.href,.assign(), or.replace().NextResponse.redirect()/rewrite()wrote raw userland input into framework routing headers. This differed from Next.js, which validates these helper URLs and rejects relative or malformed values.Next.js references
Router dangerous URL blocking:
push: packages/next/src/client/components/app-router-instance.ts#L343replace: packages/next/src/client/components/app-router-instance.ts#L440prefetch: packages/next/src/client/components/app-router-instance.ts#L458NextResponseURL validation:validateURL: packages/next/src/server/web/utils.ts#L141-L152Approach
The dangerous-scheme error message and assertion now live in
shims/url-safety.ts, sonext/navigation,next/server, and the browser Server Action redirect sink share one policy.NextResponse.redirect()/rewrite()now mirror Next.js by passing URLs throughnew URL(String(url)). That rejects relative and malformed URLs and normalizes valid absolute URLs before header write.The Server Action redirect consumer checks
x-action-redirectbefore anywindow.location.*call. If blocked, it logs the Next.js message and returns without navigating, matching the upstream hard-navigation behavior.Deliberate divergence from Next.js
Vinext already treats
data:andvbscript:as dangerous inshims/url-safety.ts, and<Link>/<Form>use that same helper. This PR keeps that existing defense-in-depth policy for the added surfaces too.Next.js's
validateURL()rejects relative/malformed URLs but does not rejectjavascript:by itself, becausenew URL("javascript:...")is syntactically valid. Vinext applies the shared dangerous-scheme guard before URL validation so all navigation sinks stay aligned.Validation
vp test run tests/shims.test.ts -t "relative URL"vp test run tests/router-javascript-urls.test.ts tests/url-safety.test.tsPLAYWRIGHT_PROJECT=app-router vp run test:e2e tests/e2e/app-router/nextjs-compat/javascript-urls.spec.tsvp check <changed files>vp run vinext#buildFull
vp checkcurrently fails on an unrelated local-only ignored note file undernathan/, not on this PR's changed files.🔄 This issue represents a GitHub Pull Request. It cannot be merged through Gitea due to API limitations.