Skip to content

Conversation

@schiller-manuel
Copy link
Contributor

@schiller-manuel schiller-manuel commented Jan 8, 2026

Summary by CodeRabbit

  • New Features

    • JSON-LD structured data support in route metadata for improved SEO.
  • Security Improvements

    • Blocks dangerous link protocols to prevent unsafe navigations.
    • Escapes embedded JSON in injected scripts to mitigate script injection.
    • Validates prerender and SPA mask paths to avoid SSRF.
    • Strengthens server-side origin detection for safer SSR.
  • Tests

    • Added comprehensive tests for protocol guards, HTML escaping, redirects, and origin handling.

✏️ Tip: You can customize this high-level summary in your review settings.

The getOrigin function was trusting the Origin header sent by clients,
which could be spoofed by attackers. This could lead to SSRF-like
vulnerabilities where the router's origin is set to a malicious value,
affecting how redirects are processed.

Now we derive the origin solely from request.url, which is set by the
server infrastructure and not directly controllable by clients.

For applications behind proxies that need custom origins, the router's
'origin' option should be used to explicitly configure a trusted origin.
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Jan 8, 2026

📝 Walkthrough

Walkthrough

Adds URL-protocol guards and HTML-escaping utilities across router packages, normalizes/validates prerender and SPA mask paths to prevent SSRF, derives origin from request URL for SSR, and injects escaped JSON-LD in head components. Tests cover protocol detection, escaping, origin, redirects, and prerender validation.

Changes

Cohort / File(s) Summary
Core utilities & exports
packages/router-core/src/utils.ts, packages/router-core/src/index.ts
Add SAFE_URL_PROTOCOLS, isDangerousProtocol(url), and escapeHtml(str); re-export escapeHtml and isDangerousProtocol.
Redirects & navigation
packages/router-core/src/redirect.ts, packages/router-core/src/router.ts, packages/router-core/src/ssr/ssr-server.ts
Block/throw on dangerous protocols in redirects and full-document navigations; enhance redirect resolution to normalize same-origin Location headers; replace Origin-header reliance with origin derived from request.url (getOrigin).
Link handling (React / Solid / Vue)
packages/react-router/src/link.tsx, packages/solid-router/src/link.tsx, packages/vue-router/src/link.tsx
Runtime validation via isDangerousProtocol() for external and resolved internal targets; short-circuit/block navigation and emit dev warnings for dangerous schemes.
Head / JSON-LD handling (React / Solid / Vue)
packages/react-router/src/HeadContent.tsx, packages/solid-router/src/HeadContent.tsx, packages/vue-router/src/HeadContent.tsx
Support script:ld+json meta entries: serialize JSON-LD, HTML-escape with escapeHtml(), insert <script type="application/ld+json">; skip invalid JSON-LD.
Scroll restoration script safety
packages/react-router/src/scroll-restoration.tsx, packages/solid-router/src/scroll-restoration.tsx, packages/vue-router/src/scroll-restoration.tsx
Escape serialized restoreScrollOptions with escapeHtml() before injecting into server-side script.
Prerender / start plugin SSRF guards
packages/start-plugin-core/src/post-server-build.ts, packages/start-plugin-core/src/prerender.ts
Validate spa.maskPath and prerender page.path are relative/simple paths (parse against http://localhost, enforce localhost origin), normalize to pathname+search+hash, and throw on absolute/external URLs.
Tests added/updated
packages/router-core/tests/dangerous-protocols.test.ts, packages/router-core/tests/getOrigin.test.ts, packages/router-core/tests/load.test.ts, packages/router-core/tests/utils.test.ts, packages/start-plugin-core/tests/post-server-build.test.ts, packages/start-plugin-core/tests/prerender-ssrf.test.ts
New/expanded tests for isDangerousProtocol, escapeHtml, getOrigin, redirect normalization, and prerender/SPA maskPath SSRF protections.

Sequence Diagram(s)

(Skipped — changes span multiple unrelated flows and do not form a single concise multi-actor sequential diagram.)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested reviewers

  • nlynzaad
  • birkskyum

Poem

🐰 I hop through code with careful paws,
I escape the angles, mend the flaws,
JSON-LD snug in a safe little script,
Origins from URLs — no header trick or crypt,
Paths stay close to home, secure and brisk.

🚥 Pre-merge checks | ✅ 1 | ❌ 2
❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.58% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'fix: hardening' is vague and generic, using a non-descriptive term that does not convey meaningful information about the specific security improvements in the changeset. Consider using a more specific title that highlights the primary security focus, such as 'fix: add security hardening for dangerous protocols and XSS mitigation' or similar.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings

Comment @coderabbitai help to get the list of available commands and usage tips.

@nx-cloud
Copy link

nx-cloud bot commented Jan 8, 2026

View your CI Pipeline Execution ↗ for commit 72bb09d

Command Status Duration Result
nx affected --targets=test:eslint,test:unit,tes... ✅ Succeeded 9m 16s View ↗
nx run-many --target=build --exclude=examples/*... ✅ Succeeded 2s View ↗

☁️ Nx Cloud last updated this comment at 2026-01-08 23:44:10 UTC

@pkg-pr-new
Copy link

pkg-pr-new bot commented Jan 8, 2026

More templates

@tanstack/arktype-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/arktype-adapter@6337

@tanstack/eslint-plugin-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/eslint-plugin-router@6337

@tanstack/history

npm i https://pkg.pr.new/TanStack/router/@tanstack/history@6337

@tanstack/nitro-v2-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/nitro-v2-vite-plugin@6337

@tanstack/react-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router@6337

@tanstack/react-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-devtools@6337

@tanstack/react-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-router-ssr-query@6337

@tanstack/react-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start@6337

@tanstack/react-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-client@6337

@tanstack/react-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/react-start-server@6337

@tanstack/router-cli

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-cli@6337

@tanstack/router-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-core@6337

@tanstack/router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools@6337

@tanstack/router-devtools-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-devtools-core@6337

@tanstack/router-generator

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-generator@6337

@tanstack/router-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-plugin@6337

@tanstack/router-ssr-query-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-ssr-query-core@6337

@tanstack/router-utils

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-utils@6337

@tanstack/router-vite-plugin

npm i https://pkg.pr.new/TanStack/router/@tanstack/router-vite-plugin@6337

@tanstack/solid-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router@6337

@tanstack/solid-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-devtools@6337

@tanstack/solid-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-router-ssr-query@6337

@tanstack/solid-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start@6337

@tanstack/solid-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-client@6337

@tanstack/solid-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/solid-start-server@6337

@tanstack/start-client-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-client-core@6337

@tanstack/start-fn-stubs

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-fn-stubs@6337

@tanstack/start-plugin-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-plugin-core@6337

@tanstack/start-server-core

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-server-core@6337

@tanstack/start-static-server-functions

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-static-server-functions@6337

@tanstack/start-storage-context

npm i https://pkg.pr.new/TanStack/router/@tanstack/start-storage-context@6337

@tanstack/valibot-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/valibot-adapter@6337

@tanstack/virtual-file-routes

npm i https://pkg.pr.new/TanStack/router/@tanstack/virtual-file-routes@6337

@tanstack/vue-router

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router@6337

@tanstack/vue-router-devtools

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-devtools@6337

@tanstack/vue-router-ssr-query

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-router-ssr-query@6337

@tanstack/vue-start

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start@6337

@tanstack/vue-start-client

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-client@6337

@tanstack/vue-start-server

npm i https://pkg.pr.new/TanStack/router/@tanstack/vue-start-server@6337

@tanstack/zod-adapter

npm i https://pkg.pr.new/TanStack/router/@tanstack/zod-adapter@6337

commit: 72bb09d

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Fix all issues with AI agents
In @packages/router-core/tests/dangerous-protocols.test.ts:
- Around line 5-99: Add targeted tests to
packages/router-core/tests/dangerous-protocols.test.ts to cover URL-encoded
schemes, null/undefined inputs, protocol-relative URLs, and malformed/very long
inputs: extend the existing describe blocks (or add a new "additional edge
cases" block) and add expectations calling isDangerousProtocol() with encoded
payloads like '%6a%61%76%61%73%63%72%69%70%74:alert(1)', with null and undefined
(expect false), with protocol-relative URLs like '//example.com' (expect false
for safe resolution), and with malformed/very long strings (e.g., extremely long
repeated 'javascript:' or non-printable bytes) to assert the function fails-safe
(true for dangerous encoded or malformed javascript, false for benign
long/unknown safe patterns); reference isDangerousProtocol to locate where to
add these new it() cases.

In @packages/start-plugin-core/src/prerender.ts:
- Around line 152-162: The URL constructor is being given routerBasePath (a path
like '/base') as the base which must be an absolute URL; update the
normalization in the startConfig.pages mapping to construct the base as an
absolute origin plus the routerBasePath (e.g., 'http://localhost' +
routerBasePath, ensuring routerBasePath starts with '/'), then create new
URL(page.path, absoluteBase) and continue enforcing origin ===
'http://localhost' and returning path: url.pathname + url.search + url.hash;
this prevents the TypeError while preserving the relative-path enforcement logic
in the startConfig.pages normalization.
🧹 Nitpick comments (2)
packages/router-core/tests/dangerous-protocols.test.ts (1)

101-132: Consider testing additional dangerous protocols in redirect suite.

The redirect tests cover the primary threat vectors (javascript:, data:) but don't test all dangerous protocols identified in the isDangerousProtocol suite. For consistency and completeness, consider adding:

  1. vbscript: and file: protocols in redirect tests
  2. Relative paths with href to ensure they work correctly (currently only tests to field without href)
📋 Suggested additional redirect test cases
   it('should throw when href uses data: protocol', () => {
     expect(() =>
       redirect({ href: 'data:text/html,<script>alert(1)</script>' }),
     ).toThrow(/unsafe protocol/)
   })
 
+  it('should throw when href uses vbscript: protocol', () => {
+    expect(() => redirect({ href: 'vbscript:msgbox(1)' })).toThrow(
+      /unsafe protocol/,
+    )
+  })
+
+  it('should throw when href uses file: protocol', () => {
+    expect(() => redirect({ href: 'file:///etc/passwd' })).toThrow(
+      /unsafe protocol/,
+    )
+  })
+
   it('should allow safe protocols', () => {
     expect(() => redirect({ href: 'https://example.com' })).not.toThrow()
     expect(() => redirect({ href: 'http://example.com' })).not.toThrow()
     expect(() => redirect({ href: 'mailto:[email protected]' })).not.toThrow()
   })
 
+  it('should allow relative paths with href', () => {
+    expect(() => redirect({ href: '/home' })).not.toThrow()
+    expect(() => redirect({ href: './relative' })).not.toThrow()
+  })
+
   it('should allow redirects without href', () => {
     expect(() => redirect({ to: '/home' })).not.toThrow()
   })
packages/start-plugin-core/tests/post-server-build.test.ts (1)

28-45: Add positive test case for valid relative paths.

The test correctly validates rejection of absolute URLs, but consider adding a test to ensure valid relative paths (e.g., '/spa', '/shell') are accepted without errors. This provides confidence that the validation doesn't inadvertently block legitimate configurations.

✅ Suggested test case
it('accepts relative SPA maskPath', async () => {
  const startConfig = {
    spa: {
      enabled: true,
      maskPath: '/spa-shell',
      prerender: {},
    },
    pages: [],
    router: { basepath: '' },
    serverFns: { base: '' },
    prerender: { enabled: true },
    sitemap: { enabled: false },
  } as any

  await expect(postServerBuild({ builder, startConfig })).resolves.not.toThrow()
})
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between a1324bb and c8b8f09.

📒 Files selected for processing (22)
  • packages/react-router/src/HeadContent.tsx
  • packages/react-router/src/link.tsx
  • packages/react-router/src/scroll-restoration.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/redirect.ts
  • packages/router-core/src/router.ts
  • packages/router-core/src/ssr/ssr-server.ts
  • packages/router-core/src/utils.ts
  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/router-core/tests/getOrigin.test.ts
  • packages/router-core/tests/load.test.ts
  • packages/router-core/tests/utils.test.ts
  • packages/solid-router/src/HeadContent.tsx
  • packages/solid-router/src/link.tsx
  • packages/solid-router/src/scroll-restoration.tsx
  • packages/start-plugin-core/src/post-server-build.ts
  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
  • packages/vue-router/src/HeadContent.tsx
  • packages/vue-router/src/link.tsx
  • packages/vue-router/src/scroll-restoration.tsx
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/router-core/src/redirect.ts
  • packages/router-core/tests/load.test.ts
  • packages/solid-router/src/HeadContent.tsx
  • packages/vue-router/src/HeadContent.tsx
  • packages/router-core/src/index.ts
  • packages/solid-router/src/scroll-restoration.tsx
  • packages/react-router/src/HeadContent.tsx
  • packages/router-core/src/router.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
  • packages/vue-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/router-core/tests/getOrigin.test.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/router-core/src/utils.ts
  • packages/start-plugin-core/src/prerender.ts
  • packages/router-core/tests/utils.test.ts
  • packages/router-core/src/ssr/ssr-server.ts
  • packages/react-router/src/scroll-restoration.tsx
  • packages/vue-router/src/scroll-restoration.tsx
  • packages/start-plugin-core/src/post-server-build.ts
  • packages/react-router/src/link.tsx
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/router-core/src/redirect.ts
  • packages/router-core/tests/load.test.ts
  • packages/solid-router/src/HeadContent.tsx
  • packages/vue-router/src/HeadContent.tsx
  • packages/router-core/src/index.ts
  • packages/solid-router/src/scroll-restoration.tsx
  • packages/react-router/src/HeadContent.tsx
  • packages/router-core/src/router.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
  • packages/vue-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/router-core/tests/getOrigin.test.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/router-core/src/utils.ts
  • packages/start-plugin-core/src/prerender.ts
  • packages/router-core/tests/utils.test.ts
  • packages/router-core/src/ssr/ssr-server.ts
  • packages/react-router/src/scroll-restoration.tsx
  • packages/vue-router/src/scroll-restoration.tsx
  • packages/start-plugin-core/src/post-server-build.ts
  • packages/react-router/src/link.tsx
🧠 Learnings (8)
📓 Common learnings
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/tests/non-nested-paths.spec.ts:167-172
Timestamp: 2025-09-22T00:56:53.426Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments during path parsing, but preserved in base path segments. This is the expected behavior implemented in PR #5182.
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
Learnt from: nlynzaad
Repo: TanStack/router PR: 5182
File: e2e/react-router/basic-file-based/src/routes/non-nested/named/$baz_.bar.tsx:3-5
Timestamp: 2025-09-22T00:56:49.237Z
Learning: In TanStack Router, underscores are intentionally stripped from route segments (e.g., `$baz_` becomes `baz` in generated types) but should be preserved in base path segments. This is the correct behavior as of the fix in PR #5182.
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/router-core/src/redirect.ts
  • packages/router-core/tests/load.test.ts
  • packages/router-core/src/index.ts
  • packages/solid-router/src/scroll-restoration.tsx
  • packages/router-core/src/router.ts
  • packages/vue-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/router-core/tests/getOrigin.test.ts
  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/router-core/src/utils.ts
  • packages/start-plugin-core/src/prerender.ts
  • packages/router-core/src/ssr/ssr-server.ts
  • packages/start-plugin-core/src/post-server-build.ts
  • packages/react-router/src/link.tsx
📚 Learning: 2025-12-06T15:03:07.223Z
Learnt from: CR
Repo: TanStack/router PR: 0
File: AGENTS.md:0-0
Timestamp: 2025-12-06T15:03:07.223Z
Learning: Applies to **/*.{js,ts,tsx} : Implement ESLint rules for router best practices using the ESLint plugin router

Applied to files:

  • packages/router-core/src/redirect.ts
  • packages/react-router/src/HeadContent.tsx
  • packages/router-core/src/router.ts
  • packages/vue-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/react-router/src/link.tsx
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/tests/load.test.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
  • packages/router-core/tests/getOrigin.test.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/router-core/tests/utils.test.ts
📚 Learning: 2025-12-17T02:17:55.086Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6120
File: packages/router-generator/src/generator.ts:654-657
Timestamp: 2025-12-17T02:17:55.086Z
Learning: In `packages/router-generator/src/generator.ts`, pathless_layout routes must receive a `path` property when they have a `cleanedPath`, even though they are non-path routes. This is necessary because child routes inherit the path from their parent, and without this property, child routes would not have the correct full path at runtime.

Applied to files:

  • packages/router-core/tests/load.test.ts
  • packages/solid-router/src/HeadContent.tsx
  • packages/router-core/src/index.ts
  • packages/router-core/src/router.ts
  • packages/solid-router/src/link.tsx
  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/src/post-server-build.ts
📚 Learning: 2025-12-21T12:52:35.231Z
Learnt from: Sheraff
Repo: TanStack/router PR: 6171
File: packages/router-core/src/new-process-route-tree.ts:898-898
Timestamp: 2025-12-21T12:52:35.231Z
Learning: In `packages/router-core/src/new-process-route-tree.ts`, the matching logic intentionally allows paths without trailing slashes to match index routes with trailing slashes (e.g., `/a` can match `/a/` route), but not vice-versa (e.g., `/a/` cannot match `/a` layout route). This is implemented via the condition `!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX` and is a deliberate design decision to provide better UX by being permissive with missing trailing slashes.

Applied to files:

  • packages/router-core/tests/load.test.ts
  • packages/router-core/src/index.ts
  • packages/router-core/src/router.ts
  • packages/vue-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/start-plugin-core/src/prerender.ts
  • packages/react-router/src/link.tsx
📚 Learning: 2025-08-30T09:12:13.852Z
Learnt from: Sheraff
Repo: TanStack/router PR: 5051
File: packages/router-core/src/utils.ts:310-315
Timestamp: 2025-08-30T09:12:13.852Z
Learning: In TanStack Router's deepEqual utility, using for..in instead of Object.keys() in getObjectKeys() when ignoreUndefined=true is acceptable because it's called only after isPlainObject() checks, which ensure objects have standard Object prototype chains with no inherited enumerable properties.

Applied to files:

  • packages/router-core/src/router.ts
  • packages/vue-router/src/link.tsx
  • packages/solid-router/src/link.tsx
  • packages/react-router/src/link.tsx
📚 Learning: 2025-12-25T13:04:55.492Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 6215
File: e2e/react-start/custom-basepath/package.json:13-17
Timestamp: 2025-12-25T13:04:55.492Z
Learning: In the TanStack Router repository, e2e test scripts are specifically designed to run in CI (which uses a Unix environment), so Unix-specific commands (like `rm -rf`, `&` for backgrounding, and direct environment variable assignments without `cross-env`) are acceptable in e2e test npm scripts.

Applied to files:

  • packages/start-plugin-core/tests/post-server-build.test.ts
🧬 Code graph analysis (9)
packages/react-router/src/HeadContent.tsx (1)
packages/router-core/src/utils.ts (1)
  • escapeHtml (575-577)
packages/router-core/src/router.ts (2)
packages/router-core/src/utils.ts (1)
  • isDangerousProtocol (541-554)
packages/router-core/src/redirect.ts (1)
  • redirect (75-118)
packages/start-plugin-core/tests/prerender-ssrf.test.ts (2)
packages/start-plugin-core/src/constants.ts (1)
  • VITE_ENVIRONMENT_NAMES (1-6)
packages/start-plugin-core/src/prerender.ts (1)
  • prerender (11-298)
packages/router-core/tests/getOrigin.test.ts (1)
packages/router-core/src/ssr/ssr-server.ts (1)
  • getOrigin (345-350)
packages/start-plugin-core/tests/post-server-build.test.ts (2)
packages/start-plugin-core/src/constants.ts (1)
  • VITE_ENVIRONMENT_NAMES (1-6)
packages/start-plugin-core/src/post-server-build.ts (1)
  • postServerBuild (8-73)
packages/router-core/tests/utils.test.ts (1)
packages/router-core/src/utils.ts (1)
  • escapeHtml (575-577)
packages/react-router/src/scroll-restoration.tsx (2)
packages/router-core/src/index.ts (2)
  • restoreScroll (380-380)
  • escapeHtml (276-276)
packages/router-core/src/utils.ts (1)
  • escapeHtml (575-577)
packages/vue-router/src/scroll-restoration.tsx (2)
packages/router-core/src/index.ts (1)
  • escapeHtml (276-276)
packages/router-core/src/utils.ts (1)
  • escapeHtml (575-577)
packages/react-router/src/link.tsx (2)
packages/router-core/src/index.ts (1)
  • isDangerousProtocol (277-277)
packages/router-core/src/utils.ts (1)
  • isDangerousProtocol (541-554)
🪛 Biome (2.1.2)
packages/solid-router/src/scroll-restoration.tsx

[error] 41-41: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

packages/react-router/src/scroll-restoration.tsx

[error] 41-41: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

packages/vue-router/src/scroll-restoration.tsx

[error] 69-69: Avoid passing children using a prop

The canonical way to pass children in React is to use JSX elements

(lint/correctness/noChildrenProp)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (24)
packages/router-core/src/ssr/ssr-server.ts (1)

331-350: Excellent SSRF protection implementation.

The security approach is sound: deriving origin from request.url (server-controlled) rather than the Origin header (client-controlled) correctly prevents SSRF vulnerabilities. The comprehensive security comment is valuable documentation for future maintainers.

One minor observation: The fallback to 'http://localhost' when URL parsing fails is safe but might mask severe infrastructure issues where request.url itself is malformed. This is likely acceptable for resilience, but consider logging such failures if not already handled upstream.

packages/router-core/src/utils.ts (2)

521-554: Robust dangerous protocol detection.

The implementation correctly uses a whitelist approach (fail-safe) and leverages the URL constructor for protocol normalization, which automatically handles bypass attempts like mixed case (JavaScript:), embedded whitespace (java\nscript:), and other encoding tricks per the WHATWG URL specification.

The logic correctly treats relative URLs (URL constructor throws) as safe since they cannot contain dangerous protocols.


556-577: Proper HTML escaping for JSON in script tags.

The implementation correctly escapes HTML-sensitive characters using Unicode escape sequences. When the escaped JSON string is embedded in a <script> tag:

  1. The HTML parser treats sequences like \u003c as literal text (not special in HTML)
  2. The JavaScript parser interprets \u003c back as < within the string context
  3. This prevents XSS: even if input contains </script>, it becomes \u003c/script\u003e which won't close the script tag

The inclusion of line/paragraph separators (U+2028, U+2029) is important as these can break JavaScript string literals.

packages/vue-router/src/scroll-restoration.tsx (1)

69-69: Correct XSS protection for embedded JSON.

The use of escapeHtml on JSON.stringify(restoreScrollOptions) correctly prevents XSS vulnerabilities if user-controlled data flows into the scroll restoration options. The escaped JSON is safe to embed in the inline script tag.

Note: The Biome static analysis warning about the children prop is a false positive. ScriptOnce requires a string (script content), not JSX children, so passing children as a prop is intentional and correct for this use case.

packages/solid-router/src/scroll-restoration.tsx (1)

41-41: Correct XSS protection for embedded JSON.

The use of escapeHtml on JSON.stringify(restoreScrollOptions) correctly prevents XSS vulnerabilities if user-controlled data flows into the scroll restoration options.

Note: The Biome static analysis warning about the children prop is a false positive. ScriptOnce requires a string (script content), not JSX children, so this usage is intentional and correct.

packages/react-router/src/scroll-restoration.tsx (1)

41-41: Correct XSS protection for embedded JSON.

The use of escapeHtml on JSON.stringify(restoreScrollOptions) correctly prevents XSS vulnerabilities if user-controlled data flows into the scroll restoration options. The implementation is consistent across all three framework adapters (React, Vue, Solid), which aids maintainability.

Note: The Biome static analysis warning about the children prop is a false positive. ScriptOnce requires a string (script content), not JSX children.

packages/router-core/src/index.ts (1)

276-277: LGTM! New security utilities exposed via public API.

The addition of escapeHtml and isDangerousProtocol to the public API enables consumers to leverage these security utilities in their own code, which aligns well with the PR's security hardening objectives.

packages/router-core/src/redirect.ts (1)

86-91: LGTM! Critical security guard for dangerous protocols.

The validation correctly blocks dangerous protocols before any URL processing occurs. Note that this security check always throws an Error (not a Response), which means it bypasses the opts.throw option—this is the correct behavior since security violations should not be bypassable.

packages/router-core/tests/utils.test.ts (1)

669-725: LGTM! Comprehensive test coverage for HTML escaping.

The test suite thoroughly validates escapeHtml functionality, including:

  • All targeted special characters (<, >, &, Unicode line/paragraph separators)
  • XSS prevention scenarios (script tag injection in JSON)
  • Edge cases (empty strings, strings without special characters)
  • Mixed content handling

The tests correctly verify that escaped output uses Unicode escape sequences and doesn't contain literal dangerous characters.

packages/router-core/tests/getOrigin.test.ts (1)

1-62: LGTM! Excellent SSRF-prevention test coverage.

This test suite effectively validates that getOrigin mitigates SSRF-like attacks by:

  • Deriving origin exclusively from request.url, not spoofable headers
  • Providing safe fallback behavior (http://localhost) for invalid URLs
  • Never trusting the Origin header, even when URL parsing fails

The tests directly address CVE-2024-34351 concerns and provide strong confidence in the security posture of origin detection.

packages/vue-router/src/HeadContent.tsx (1)

33-47: LGTM! Secure JSON-LD handling with proper escaping.

The implementation correctly:

  • Applies escapeHtml after JSON.stringify to prevent XSS injection via innerHTML
  • Uses Unicode escape sequences (\u003c, \u003e) which are valid in JSON strings
  • Creates properly typed script tags (application/ld+json)
  • Silently skips invalid JSON-LD objects to fail safely

The try/catch pattern prioritizes security by preventing crashes, though it may make debugging slightly harder for developers with malformed JSON-LD data.

packages/react-router/src/HeadContent.tsx (1)

38-52: LGTM! JSON-LD handling with proper XSS mitigation.

The implementation correctly escapes HTML entities in the serialized JSON to prevent XSS when the content is injected into the DOM. The try/catch appropriately handles edge cases like circular references in JSON-LD objects.

packages/router-core/tests/load.test.ts (1)

16-47: LGTM! Good test coverage for redirect resolution behavior.

This test correctly validates the same-origin normalization logic in resolveRedirect. The assertions ensure both the Location header and options.href are normalized to path-only format, which aligns with the intentional design to strip the router's origin from redirect URLs when they match.

packages/router-core/src/router.ts (2)

2052-2061: LGTM! Critical security guard for document navigation.

The dangerous protocol check correctly prevents XSS attacks via javascript:, data:, or vbscript: URLs before they can be assigned to window.location. The warning in non-production helps developers identify misuse.


2451-2474: LGTM! Correct same-origin normalization for redirects.

The enhanced resolveRedirect logic properly normalizes same-origin absolute URLs in the Location header to path-only format (e.g., https://example.com/foo/foo), consistent with the intentional design. The try/catch correctly handles invalid URLs without breaking the flow.

packages/vue-router/src/link.tsx (1)

253-285: LGTM! Dangerous protocol guard for external links.

The implementation correctly blocks dangerous protocols by returning props without the href attribute, which prevents navigation while maintaining the element's other properties for accessibility. The dev-only warning helps identify misuse.

packages/solid-router/src/link.tsx (1)

161-182: LGTM! Comprehensive dangerous protocol guards.

The implementation correctly guards both external links (via hrefOption.href) and internal navigation targets (via options.to) against dangerous protocols. Returning undefined prevents navigation while the dev-only warnings help identify misuse.

packages/solid-router/src/HeadContent.tsx (1)

37-51: LGTM! JSON-LD handling consistent with React router.

The implementation correctly mirrors the React router's JSON-LD handling with proper HTML escaping to prevent XSS. The try/catch appropriately handles invalid JSON-LD objects.

packages/react-router/src/link.tsx (1)

146-171: LGTM! Comprehensive dangerous protocol guards for React.

The implementation correctly guards both external links (line 149) and internal navigation targets (line 162) against dangerous protocols like javascript:, data:, and vbscript:. The dev-only warnings and undefined returns effectively block navigation attempts.

packages/router-core/tests/dangerous-protocols.test.ts (1)

1-4: LGTM! Clean test setup.

The imports are well-organized and appropriate for testing dangerous protocol validation.

packages/start-plugin-core/src/post-server-build.ts (1)

36-39: LGTM - Effective SSRF prevention.

The validation correctly prevents absolute URLs from being used as spa.maskPath by leveraging the URL constructor's behavior: relative paths preserve the base origin while absolute URLs replace it. This guards against external prerendering attacks.

packages/start-plugin-core/tests/prerender-ssrf.test.ts (2)

50-59: LGTM - Comprehensive SSRF protection test.

The test effectively validates that absolute external URLs are rejected before any network requests are made, preventing potential SSRF attacks. The fetchMock assertion confirms no side effects occur.


61-66: LGTM - Validates legitimate paths work correctly.

The positive test case ensures that the SSRF protection doesn't inadvertently block valid relative paths, providing balanced test coverage.

packages/start-plugin-core/src/prerender.ts (1)

43-64: LGTM - Robust path normalization and SSRF protection.

The initial path validation correctly:

  • Creates a proper base URL for parsing
  • Catches and wraps URL parsing errors with context
  • Validates origin to ensure paths are relative
  • Normalizes paths to pathname + search + hash

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @packages/start-plugin-core/src/prerender.ts:
- Around line 152-172: Extract the duplicated page validation/normalization
logic into a helper function named validateAndNormalizePrerenderPages that
accepts pages and a routerBaseUrl (URL) and returns the normalized Page[]; move
the try/catch that constructs new URL(page.path, routerBaseUrl), the origin
check for 'http://localhost', and the return of {...page, path: url.pathname +
url.search + url.hash} into that helper (preserve the Error with cause on URL
construction). Replace both inline usages (the array.map in prerender.ts and the
nearly identical block at the other occurrence) by calling
validateAndNormalizePrerenderPages(startConfig.pages, new URL(absoluteBase)) (or
equivalent) to keep behavior identical while removing duplication.
🧹 Nitpick comments (1)
packages/start-plugin-core/src/prerender.ts (1)

128-141: Consider applying URL validation to crawled links for defense-in-depth.

While extractLinks filters to relative paths (/ or ./ prefixes), these crawled links bypass the URL validation applied to initial pages (lines 46-64). This creates an inconsistency in security posture.

Notably, protocol-relative URLs like //evil.com/path would pass the href.startsWith('/') check, potentially allowing SSRF if not caught downstream.

Consider applying the same validation when adding crawled pages:

// In addCrawlPageTask or when calling it at line 288
for (const link of links) {
  try {
    const url = new URL(link, routerBaseUrl)
    if (url.origin !== 'http://localhost') {
      continue // Skip external links discovered during crawl
    }
    const normalizedPath = url.pathname + url.search + url.hash
    addCrawlPageTask({ path: normalizedPath, fromCrawl: true })
  } catch {
    // Skip invalid URLs
  }
}

Alternatively, update extractLinks to be more restrictive:

-if (href && (href.startsWith('/') || href.startsWith('./'))) {
+if (href && href.startsWith('/') && !href.startsWith('//')) {
   links.push(href)
 }

Also applies to: 288-288

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d32bf01 and 55b0f52.

📒 Files selected for processing (2)
  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/start-plugin-core/src/prerender.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/start-plugin-core/src/prerender.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/start-plugin-core/src/prerender.ts
🧠 Learnings (5)
📓 Common learnings
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/router-core/tests/dangerous-protocols.test.ts
  • packages/start-plugin-core/src/prerender.ts
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/router-core/tests/dangerous-protocols.test.ts
📚 Learning: 2025-12-17T02:17:55.086Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6120
File: packages/router-generator/src/generator.ts:654-657
Timestamp: 2025-12-17T02:17:55.086Z
Learning: In `packages/router-generator/src/generator.ts`, pathless_layout routes must receive a `path` property when they have a `cleanedPath`, even though they are non-path routes. This is necessary because child routes inherit the path from their parent, and without this property, child routes would not have the correct full path at runtime.

Applied to files:

  • packages/start-plugin-core/src/prerender.ts
📚 Learning: 2025-12-21T12:52:35.231Z
Learnt from: Sheraff
Repo: TanStack/router PR: 6171
File: packages/router-core/src/new-process-route-tree.ts:898-898
Timestamp: 2025-12-21T12:52:35.231Z
Learning: In `packages/router-core/src/new-process-route-tree.ts`, the matching logic intentionally allows paths without trailing slashes to match index routes with trailing slashes (e.g., `/a` can match `/a/` route), but not vice-versa (e.g., `/a/` cannot match `/a` layout route). This is implemented via the condition `!pathIsIndex || node.kind === SEGMENT_TYPE_INDEX` and is a deliberate design decision to provide better UX by being permissive with missing trailing slashes.

Applied to files:

  • packages/start-plugin-core/src/prerender.ts
🧬 Code graph analysis (1)
packages/router-core/tests/dangerous-protocols.test.ts (2)
packages/router-core/src/utils.ts (1)
  • isDangerousProtocol (541-554)
packages/router-core/src/redirect.ts (1)
  • redirect (75-118)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (3)
packages/router-core/tests/dangerous-protocols.test.ts (2)

5-234: Excellent comprehensive test coverage for dangerous protocol detection.

This test suite thoroughly covers the security-critical isDangerousProtocol function with:

  • All major dangerous protocols (javascript:, data:, vbscript:, file:, custom)
  • Bypass attempt variations (newlines, mixed case, leading whitespace)
  • Safe protocols verification (http:, https:, mailto:, tel:)
  • Edge cases (empty input, null/undefined, relative URLs, query strings, hash fragments)
  • Advanced edge cases (URL-encoded protocols, protocol-relative URLs, unicode, non-printable characters, long payloads)

The comments effectively explain nuanced behavior, particularly around URL-encoded protocols (lines 112-119) where the URL constructor correctly doesn't decode percent-encoding in protocols, matching browser behavior.

The type assertions for null/undefined (lines 103, 107) appropriately test runtime behavior beyond TypeScript's type system.


236-267: LGTM! Redirect security tests cover the critical paths.

The redirect tests appropriately verify:

  • Dangerous protocols (javascript:, data:) are blocked with error
  • Bypass attempts are caught
  • Safe protocols are allowed
  • Redirects using the to parameter (without href) work correctly

The error message matcher /unsafe protocol/ provides good balance between specificity and maintainability.

packages/start-plugin-core/src/prerender.ts (1)

43-64: Effective SSRF prevention through URL validation.

The validation logic correctly enforces relative paths by:

  • Constructing URLs with a localhost base to resolve relative paths
  • Rejecting any URL with a non-localhost origin
  • Normalizing to pathname + search + hash to strip protocol/host

This prevents malicious absolute URLs from being used in prerendering.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Fix all issues with AI agents
In @packages/start-plugin-core/src/prerender.ts:
- Around line 313-347: validateAndNormalizePrerenderPages: replace the hardcoded
origin check and guard against decodeURIComponent throwing; specifically, use
routerBaseUrl.origin for the origin comparison (i.e., check url.origin !==
routerBaseUrl.origin) and wrap the decodeURIComponent(url.pathname) call in a
try-catch that throws a clear Error (including the original error as cause) on
URIError/malformed percent-encoding so malformed paths are rejected safely while
preserving the original behavior of returning page objects with decoded pathname
+ url.search + url.hash.
🧹 Nitpick comments (2)
packages/start-plugin-core/src/prerender.ts (1)

138-143: Consider removing redundant normalization.

The pages are already normalized at lines 47-50 after merging discovered pages. This second normalization call happens before any new pages are added from crawling (line 259), so it appears redundant.

If the intent is to re-validate pages added during crawling, consider moving this validation to addCrawlPageTask when pages are added with fromCrawl: true.

packages/start-plugin-core/tests/prerender-ssrf.test.ts (1)

75-92: Consider expanding test coverage for edge cases.

The current tests effectively validate the core SSRF protection (absolute URLs rejected, relative paths allowed). Consider adding tests for additional edge cases to ensure comprehensive protection:

  • Protocol-relative URLs: //attacker.test/leak
  • Other potentially dangerous protocols: file:///etc/passwd, data:text/html,<script>...
  • Unicode paths to verify decoding: /대한민국 or /café
  • Malformed percent-encoding: /path%2
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55b0f52 and 6ae0893.

📒 Files selected for processing (2)
  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
🧠 Learnings (4)
📓 Common learnings
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
📚 Learning: 2025-12-17T02:17:55.086Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6120
File: packages/router-generator/src/generator.ts:654-657
Timestamp: 2025-12-17T02:17:55.086Z
Learning: In `packages/router-generator/src/generator.ts`, pathless_layout routes must receive a `path` property when they have a `cleanedPath`, even though they are non-path routes. This is necessary because child routes inherit the path from their parent, and without this property, child routes would not have the correct full path at runtime.

Applied to files:

  • packages/start-plugin-core/src/prerender.ts
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/start-plugin-core/src/prerender.ts
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
🧬 Code graph analysis (1)
packages/start-plugin-core/src/prerender.ts (1)
packages/start-plugin-core/src/schema.ts (1)
  • Page (202-202)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (1)
packages/start-plugin-core/src/prerender.ts (1)

43-50: LGTM! Good SSRF protection for prerender pages.

The validation ensures that all prerender pages are relative paths and rejects absolute URLs that could lead to SSRF vulnerabilities.

Comment on lines +313 to +347

/**
* Validates and normalizes prerender page paths to ensure they are relative
* (no protocol/host) and returns normalized Page objects with cleaned paths.
* Preserves unicode characters by decoding the pathname after URL validation.
*/
function validateAndNormalizePrerenderPages(
pages: Array<Page>,
routerBaseUrl: URL,
): Array<Page> {
return pages.map((page) => {
let url: URL
try {
url = new URL(page.path, routerBaseUrl)
} catch (err) {
throw new Error(`prerender page path must be relative: ${page.path}`, {
cause: err,
})
}

if (url.origin !== 'http://localhost') {
throw new Error(`prerender page path must be relative: ${page.path}`)
}

// Decode the pathname to preserve unicode characters (e.g., /대한민국)
// The URL constructor encodes non-ASCII characters, but we want to keep
// the original unicode form for filesystem paths
const decodedPathname = decodeURIComponent(url.pathname)

return {
...page,
path: decodedPathname + url.search + url.hash,
}
})
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle potential decoding errors and prefer dynamic origin check.

The validation logic is sound for SSRF protection. Two improvements to consider:

  1. Line 340 - Handle malformed percent-encoding: The decodeURIComponent call could throw a URIError if the pathname contains malformed percent-encoding (e.g., incomplete escape sequences). Wrap it in a try-catch for robustness.

  2. Line 333 - Use dynamic origin check: The hardcoded 'http://localhost' check works but is brittle. Use routerBaseUrl.origin instead to make the validation more maintainable and robust to future changes.

🔧 Proposed improvements
 function validateAndNormalizePrerenderPages(
   pages: Array<Page>,
   routerBaseUrl: URL,
 ): Array<Page> {
   return pages.map((page) => {
     let url: URL
     try {
       url = new URL(page.path, routerBaseUrl)
     } catch (err) {
       throw new Error(`prerender page path must be relative: ${page.path}`, {
         cause: err,
       })
     }
 
-    if (url.origin !== 'http://localhost') {
+    if (url.origin !== routerBaseUrl.origin) {
       throw new Error(`prerender page path must be relative: ${page.path}`)
     }
 
     // Decode the pathname to preserve unicode characters (e.g., /대한민국)
     // The URL constructor encodes non-ASCII characters, but we want to keep
     // the original unicode form for filesystem paths
-    const decodedPathname = decodeURIComponent(url.pathname)
+    let decodedPathname: string
+    try {
+      decodedPathname = decodeURIComponent(url.pathname)
+    } catch (err) {
+      throw new Error(
+        `prerender page path contains invalid percent-encoding: ${page.path}`,
+        { cause: err },
+      )
+    }
 
     return {
       ...page,
       path: decodedPathname + url.search + url.hash,
     }
   })
 }
🤖 Prompt for AI Agents
In @packages/start-plugin-core/src/prerender.ts around lines 313 - 347,
validateAndNormalizePrerenderPages: replace the hardcoded origin check and guard
against decodeURIComponent throwing; specifically, use routerBaseUrl.origin for
the origin comparison (i.e., check url.origin !== routerBaseUrl.origin) and wrap
the decodeURIComponent(url.pathname) call in a try-catch that throws a clear
Error (including the original error as cause) on URIError/malformed
percent-encoding so malformed paths are rejected safely while preserving the
original behavior of returning page objects with decoded pathname + url.search +
url.hash.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/start-plugin-core/src/prerender.ts (1)

114-127: Protocol-relative URLs bypass origin validation.

The filter at line 121 allows protocol-relative URLs like //evil.com/path because they start with /. When passed to localFetch at line 182-183, these become http://evil.com/path after URL construction (line 83), enabling SSRF to external hosts during prerendering.

Crawled links are not validated through validateAndNormalizePrerenderPages, leaving this vector unprotected.

🔒 Proposed fix to filter protocol-relative URLs
 function extractLinks(html: string): Array<string> {
   const linkRegex = /<a[^>]+href=["']([^"']+)["'][^>]*>/g
   const links: Array<string> = []
   let match

   while ((match = linkRegex.exec(html)) !== null) {
     const href = match[1]
-    if (href && (href.startsWith('/') || href.startsWith('./'))) {
+    // Exclude protocol-relative URLs (//host/path) and only allow single-slash or relative paths
+    if (href && (href.startsWith('./') || (href.startsWith('/') && !href.startsWith('//')))) {
       links.push(href)
     }
   }

   return links
 }
🧹 Nitpick comments (3)
packages/start-plugin-core/src/prerender.ts (3)

138-143: Consider removing redundant validation.

This validation call processes the same pages already validated at lines 47-50, as startConfig.pages isn't modified between those points. Pages discovered during crawling (line 159) are added after this validation, so they're never validated anyway.

If prerenderPages might be called independently in the future, consider documenting that intent; otherwise, this appears redundant.


333-335: Hard-coded origin check reduces flexibility.

Comparing against 'http://localhost' couples the validation to a specific base URL. If routerBaseUrl ever uses a different origin (e.g., in testing scenarios), this would incorrectly reject valid relative paths.

♻️ Proposed refactor
-    if (url.origin !== 'http://localhost') {
+    if (url.origin !== routerBaseUrl.origin) {
       throw new Error(`prerender page path must be relative: ${page.path}`)
     }

340-344: Inconsistent URI decoding.

Only pathname is decoded (line 340), while search and hash remain percent-encoded. This creates paths that are partially decoded. While probably intentional for filesystem compatibility, consider whether search/hash should also be decoded for consistency.

📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6ae0893 and 2125eb6.

📒 Files selected for processing (2)
  • packages/start-plugin-core/src/prerender.ts
  • packages/start-plugin-core/tests/post-server-build.test.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/start-plugin-core/tests/post-server-build.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/start-plugin-core/src/prerender.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/start-plugin-core/src/prerender.ts
🧠 Learnings (3)
📓 Common learnings
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
📚 Learning: 2025-10-01T18:30:26.591Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.

Applied to files:

  • packages/start-plugin-core/src/prerender.ts
📚 Learning: 2025-12-17T02:17:55.086Z
Learnt from: schiller-manuel
Repo: TanStack/router PR: 6120
File: packages/router-generator/src/generator.ts:654-657
Timestamp: 2025-12-17T02:17:55.086Z
Learning: In `packages/router-generator/src/generator.ts`, pathless_layout routes must receive a `path` property when they have a `cleanedPath`, even though they are non-path routes. This is necessary because child routes inherit the path from their parent, and without this property, child routes would not have the correct full path at runtime.

Applied to files:

  • packages/start-plugin-core/src/prerender.ts
🧬 Code graph analysis (1)
packages/start-plugin-core/src/prerender.ts (1)
packages/start-plugin-core/src/schema.ts (1)
  • Page (202-202)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Test
  • GitHub Check: Preview
🔇 Additional comments (1)
packages/start-plugin-core/src/prerender.ts (1)

43-50: Good: SSRF protection for initial prerender pages.

The validation correctly ensures that user-defined prerender pages are relative paths without external protocols or hosts.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (2)
packages/start-plugin-core/tests/prerender-ssrf.test.ts (2)

13-18: Consider removing the unnecessary prerender mock.

This mock simply re-exports the actual implementation without any modifications. Since the prerender function is what's being tested (imported on line 2), this mock serves no purpose and can be safely removed.

♻️ Simplify by removing the mock
-vi.mock('../src/prerender', async () => {
-  const actual = await vi.importActual<any>('../src/prerender')
-  return {
-    ...actual,
-  }
-})
-

94-99: Strengthen the positive test case with additional assertions.

The test verifies that relative paths don't throw an error, but doesn't confirm that prerender actually performs its work. Consider adding assertions to verify expected behavior, such as:

  • fetchMock was called with the correct URL
  • Filesystem operations were invoked (if observable through the mocks)
♻️ Example: Add positive assertions
  it('allows relative paths', async () => {
    resetFetch()
    const startConfig = makeStartConfig('/about')

    await expect(prerender({ startConfig, builder })).resolves.not.toThrow()
+   expect(fetchMock).toHaveBeenCalledWith(
+     expect.stringContaining('/about'),
+     expect.any(Object),
+   )
  })
📜 Review details

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2125eb6 and 72bb09d.

📒 Files selected for processing (1)
  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
🧰 Additional context used
📓 Path-based instructions (2)
**/*.{ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Use TypeScript strict mode with extensive type safety for all code

Files:

  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
**/*.{js,ts,tsx}

📄 CodeRabbit inference engine (AGENTS.md)

Implement ESLint rules for router best practices using the ESLint plugin router

Files:

  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
🧠 Learnings (2)
📓 Common learnings
Learnt from: schiller-manuel
Repo: TanStack/router PR: 5330
File: packages/router-core/src/router.ts:2231-2245
Timestamp: 2025-10-01T18:30:26.591Z
Learning: In `packages/router-core/src/router.ts`, the `resolveRedirect` method intentionally strips the router's origin from redirect URLs when they match (e.g., `https://foo.com/bar` → `/bar` for same-origin redirects) while preserving the full URL for cross-origin redirects. This logic should not be removed or simplified to use `location.publicHref` directly.
📚 Learning: 2025-10-08T08:11:47.088Z
Learnt from: nlynzaad
Repo: TanStack/router PR: 5402
File: packages/router-generator/tests/generator/no-formatted-route-tree/routeTree.nonnested.snapshot.ts:19-21
Timestamp: 2025-10-08T08:11:47.088Z
Learning: Test snapshot files in the router-generator tests directory (e.g., files matching the pattern `packages/router-generator/tests/generator/**/routeTree*.snapshot.ts` or `routeTree*.snapshot.js`) should not be modified or have issues flagged, as they are fixtures used to verify the generator's output and are intentionally preserved as-is.

Applied to files:

  • packages/start-plugin-core/tests/prerender-ssrf.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (2)
  • GitHub Check: Preview
  • GitHub Check: Test
🔇 Additional comments (1)
packages/start-plugin-core/tests/prerender-ssrf.test.ts (1)

83-92: Excellent SSRF protection test coverage.

The test properly validates that absolute external URLs are rejected before any network calls occur. The assertion that fetchMock is not called (line 91) is particularly important for confirming SSRF prevention. This aligns well with the security hardening objectives of this PR.

@schiller-manuel schiller-manuel merged commit e5fd28b into main Jan 8, 2026
6 checks passed
@schiller-manuel schiller-manuel deleted the hardening branch January 8, 2026 23:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants