mirror of
https://github.com/cloudflare/vinext.git
synced 2026-09-14 19:04:59 +08:00
f335e2da3f
* fix(app-router): block javascript: URLs in router.push/replace/prefetch
router.push("javascript:alert(1)") currently runs the script. _appRouter.push,
.replace, and .prefetch in shims/navigation.ts forward the href straight to
navigateClientSide, where isExternalUrl matches the javascript: scheme and
falls through to window.location.assign, which executes the URL. router-derived
hrefs (URL params, user content, query state) become an XSS vector.
Next.js blocks dangerous URI schemes at the router boundary and throws
"Next.js has blocked a javascript: URL as a security precaution." See
packages/next/src/client/components/app-router-instance.ts:343,400,440,458 in
the canary tree. Vinext's url-safety.isDangerousScheme already covers
javascript:/data:/vbscript: with the same obfuscation handling Next.js uses,
and Link and Form already gate on it; the App Router programmatic surface was
the remaining gap.
Add assertSafeNavigationHref at the top of push/replace/prefetch, before the
isServer short-circuit so SSR callers fail loudly too. Throw an Error with
the verbatim Next.js message so consumer error tooling can match across both
frameworks.
Test: tests/router-javascript-urls.test.ts ports the assertions from
.nextjs-ref/test/e2e/app-dir/javascript-urls/javascript-urls.test.ts down to
unit scope, covering javascript:/data:/vbscript:, uppercase, embedded tabs,
leading whitespace, and a safe-URL no-throw guard for each method.
* test(router): split ported vs vinext-only dangerous-scheme cases
The header said "Ported from Next.js" but data: and vbscript: are vinext
extensions, not in the upstream e2e suite. Add a Coverage split note that
attributes each scheme: javascript: + obfuscation variants come from
test/e2e/app-dir/javascript-urls/javascript-urls.test.ts; data: and vbscript:
ride on top via shims/url-safety.isDangerousScheme.
* fix app router dangerous redirect gaps
* test address dangerous URL review nits
94 lines
4.3 KiB
TypeScript
94 lines
4.3 KiB
TypeScript
import { describe, it, expect } from "vite-plus/test";
|
|
|
|
// Ported from Next.js: test/e2e/app-dir/javascript-urls/javascript-urls.test.ts
|
|
// https://github.com/vercel/next.js/blob/canary/test/e2e/app-dir/javascript-urls/javascript-urls.test.ts
|
|
//
|
|
// Next.js blocks dangerous URI schemes in router.push/replace/prefetch with a
|
|
// thrown Error: "Next.js has blocked a javascript: URL as a security precaution."
|
|
// See: packages/next/src/client/components/app-router-instance.ts:343,400,440,458
|
|
//
|
|
// Vinext mirrors that behavior. The guard runs before any programmatic
|
|
// navigation kicks off, at the top of push/replace/prefetch. Even server-side
|
|
// calls throw, matching Next.js intent and giving SSR-safe regression coverage
|
|
// in node-based unit tests.
|
|
//
|
|
// Coverage split:
|
|
// Ported from the Next.js E2E suite above: javascript: cases for
|
|
// push/replace/prefetch plus the obfuscation variants that Next.js's
|
|
// javascript-url.ts regex covers (uppercase, embedded tabs, leading
|
|
// whitespace).
|
|
// Vinext-only extensions: data: and vbscript: cases. Vinext intentionally
|
|
// extends the dangerous-scheme block to those schemes via the shared
|
|
// isDangerousScheme helper. Rationale and scope: shims/url-safety.ts:20-21.
|
|
|
|
const BLOCK_MESSAGE = "Next.js has blocked a javascript: URL as a security precaution.";
|
|
|
|
describe("App Router useRouter() blocks dangerous URI schemes", () => {
|
|
it("router.push throws on javascript: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push("javascript:alert(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.replace throws on javascript: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.replace("javascript:alert(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.prefetch throws on javascript: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.prefetch("javascript:alert(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.push throws on data: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push("data:text/html,<script>alert(1)</script>")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.push throws on vbscript: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push("vbscript:MsgBox(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.push throws on obfuscated javascript: URL with embedded tabs", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push("java\tscript:alert(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.push throws on uppercase JAVASCRIPT: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push("JAVASCRIPT:alert(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
it("router.push throws on leading-whitespace javascript: URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push(" javascript:alert(1)")).toThrow(BLOCK_MESSAGE);
|
|
});
|
|
|
|
// Safe URLs must not throw — guard must not over-block.
|
|
it("router.push does not throw on a normal pathname", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.push("/safe")).not.toThrow();
|
|
});
|
|
|
|
it("router.replace does not throw on absolute https URL", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.replace("https://example.com/path")).not.toThrow();
|
|
});
|
|
|
|
it("router.prefetch does not throw on a normal pathname", async () => {
|
|
const { useRouter } = await import("../packages/vinext/src/shims/navigation.js");
|
|
const router = useRouter();
|
|
expect(() => router.prefetch("/safe")).not.toThrow();
|
|
});
|
|
});
|