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
24 changes: 10 additions & 14 deletions docs/router/framework/react/api/router/RouterOptionsType.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,38 +144,34 @@ The `RouterOptions` type accepts an object with the following properties and met
- When `true`, disables the global catch boundary that normally wraps all route matches. This allows unhandled errors to bubble up to top-level error handlers in the browser.
- Useful for testing tools, error reporting services, and debugging scenarios.

### `protocolBlocklist` property
### `protocolAllowlist` property

- Type: `Array<string>`
- Optional
- Defaults to `DEFAULT_PROTOCOL_BLOCKLIST` which includes:
- Script execution: `javascript:`, `vbscript:`
- Local file access: `file:`
- Data embedding: `blob:`, `data:`
- Browser internals: `about:`
- Platform-specific: `ms-appx:`, `ms-appx-web:`, `ms-browser-extension:`, `chrome-extension:`, `moz-extension:`
- Archive/resource: `jar:`, `view-source:`, `resource:`, `wyciwyg:`
- An array of URL protocols to block in links, redirects, and navigation. URLs with these protocols will be rejected to prevent security vulnerabilities like XSS attacks.
- Defaults to `DEFAULT_PROTOCOL_ALLOWLIST` which includes:
- Web navigation: `http:`, `https:`
- Common browser-safe actions: `mailto:`, `tel:`, `sms:`
- An array of URL protocols that are allowed in links, redirects, and navigation. Absolute URLs with protocols not in this list are rejected to prevent security vulnerabilities like XSS attacks.
- The router creates a `Set` from this array internally for efficient lookup.

**Example**

```tsx
import {
createRouter,
DEFAULT_PROTOCOL_BLOCKLIST,
DEFAULT_PROTOCOL_ALLOWLIST,
} from '@tanstack/react-router'

// Use a custom blocklist (replaces the default)
// Use a custom allowlist (replaces the default)
const router = createRouter({
routeTree,
protocolBlocklist: ['javascript:', 'data:'],
protocolAllowlist: ['https:', 'mailto:'],
})

// Or extend the default blocklist
// Or extend the default allowlist
const router = createRouter({
routeTree,
protocolBlocklist: [...DEFAULT_PROTOCOL_BLOCKLIST, 'ftp:', 'gopher:'],
protocolAllowlist: [...DEFAULT_PROTOCOL_ALLOWLIST, 'ftp:'],
})
```

Expand Down
2 changes: 1 addition & 1 deletion packages/react-router/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ export {
redirect,
isRedirect,
createRouterConfig,
DEFAULT_PROTOCOL_BLOCKLIST,
DEFAULT_PROTOCOL_ALLOWLIST,
} from '@tanstack/router-core'

export {
Expand Down
12 changes: 6 additions & 6 deletions packages/react-router/src/link.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ export function useLinkProps<
) {
try {
new URL(to)
if (isDangerousProtocol(to, router.protocolBlocklist)) {
if (isDangerousProtocol(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`Blocked Link with dangerous protocol: ${to}`)
}
Expand Down Expand Up @@ -170,7 +170,7 @@ export function useLinkProps<

const externalLink = (() => {
if (hrefOption?.external) {
if (isDangerousProtocol(hrefOption.href, router.protocolBlocklist)) {
if (isDangerousProtocol(hrefOption.href, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Blocked Link with dangerous protocol: ${hrefOption.href}`,
Expand All @@ -187,7 +187,7 @@ export function useLinkProps<
if (typeof to === 'string' && to.indexOf(':') > -1) {
try {
new URL(to)
if (isDangerousProtocol(to, router.protocolBlocklist)) {
if (isDangerousProtocol(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`Blocked Link with dangerous protocol: ${to}`)
}
Expand Down Expand Up @@ -438,7 +438,7 @@ export function useLinkProps<
const externalLink = React.useMemo(() => {
if (hrefOption?.external) {
// Block dangerous protocols for external links
if (isDangerousProtocol(hrefOption.href, router.protocolBlocklist)) {
if (isDangerousProtocol(hrefOption.href, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Blocked Link with dangerous protocol: ${hrefOption.href}`,
Expand All @@ -454,7 +454,7 @@ export function useLinkProps<
try {
new URL(to as any)
// Block dangerous protocols like javascript:, blob:, data:
if (isDangerousProtocol(to, router.protocolBlocklist)) {
if (isDangerousProtocol(to, router.protocolAllowlist)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(`Blocked Link with dangerous protocol: ${to}`)
}
Expand All @@ -463,7 +463,7 @@ export function useLinkProps<
return to
} catch {}
return undefined
}, [to, hrefOption, router.protocolBlocklist])
}, [to, hrefOption, router.protocolAllowlist])

// eslint-disable-next-line react-hooks/rules-of-hooks
const isActive = useRouterState({
Expand Down
2 changes: 1 addition & 1 deletion packages/router-core/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ export {
createControlledPromise,
isModuleNotFoundError,
decodePath,
DEFAULT_PROTOCOL_BLOCKLIST,
DEFAULT_PROTOCOL_ALLOWLIST,
escapeHtml,
isDangerousProtocol,
buildDevStylesUrl,
Expand Down
26 changes: 13 additions & 13 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { createBrowserHistory, parseHref } from '@tanstack/history'
import { isServer } from '@tanstack/router-core/isServer'
import { batch } from './utils/batch'
import {
DEFAULT_PROTOCOL_BLOCKLIST,
DEFAULT_PROTOCOL_ALLOWLIST,
createControlledPromise,
decodePath,
deepEqual,
Expand Down Expand Up @@ -472,13 +472,13 @@ export interface RouterOptions<
disableGlobalCatchBoundary?: boolean

/**
* An array of URL protocols to block in links, redirects, and navigation.
* URLs with these protocols will be rejected to prevent security vulnerabilities.
* An array of URL protocols to allow in links, redirects, and navigation.
* Absolute URLs with protocols not in this list will be rejected.
*
* @default DEFAULT_PROTOCOL_BLOCKLIST (includes javascript:, vbscript:, file:, blob:, data:, about:, and browser extension protocols)
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#protocolblocklist-property)
* @default DEFAULT_PROTOCOL_ALLOWLIST (http:, https:, mailto:, tel:, sms:)
* @link [API Docs](https://tanstack.com/router/latest/docs/framework/react/api/router/RouterOptionsType#protocolallowlist-property)
*/
protocolBlocklist?: Array<string>
protocolAllowlist?: Array<string>

serializationAdapters?: ReadonlyArray<AnySerializationAdapter>
/**
Expand Down Expand Up @@ -971,7 +971,7 @@ export class RouterCore<
resolvePathCache!: LRUCache<string, string>
isServer!: boolean
pathParamsDecoder?: (encoded: string) => string
protocolBlocklist!: Set<string>
protocolAllowlist!: Set<string>

/**
* @deprecated Use the `createRouter` function instead
Expand All @@ -995,8 +995,8 @@ export class RouterCore<
notFoundMode: options.notFoundMode ?? 'fuzzy',
stringifySearch: options.stringifySearch ?? defaultStringifySearch,
parseSearch: options.parseSearch ?? defaultParseSearch,
protocolBlocklist:
options.protocolBlocklist ?? DEFAULT_PROTOCOL_BLOCKLIST,
protocolAllowlist:
options.protocolAllowlist ?? DEFAULT_PROTOCOL_ALLOWLIST,
})

if (typeof document !== 'undefined') {
Expand Down Expand Up @@ -1042,7 +1042,7 @@ export class RouterCore<

this.isServer = this.options.isServer ?? typeof document === 'undefined'

this.protocolBlocklist = new Set(this.options.protocolBlocklist)
this.protocolAllowlist = new Set(this.options.protocolAllowlist)

if (this.options.pathParamsAllowedCharacters)
this.pathParamsDecoder = compileDecodeCharMap(
Expand Down Expand Up @@ -2258,7 +2258,7 @@ export class RouterCore<

// Block dangerous protocols like javascript:, blob:, data:
// These could execute arbitrary code if passed to window.location
if (isDangerousProtocol(reloadHref, this.protocolBlocklist)) {
if (isDangerousProtocol(reloadHref, this.protocolAllowlist)) {
if (process.env.NODE_ENV !== 'production') {
console.warn(
`Blocked navigation to dangerous protocol: ${reloadHref}`,
Expand Down Expand Up @@ -2681,10 +2681,10 @@ export class RouterCore<
redirect.options.href &&
!redirect.options._builtLocation &&
// Check for dangerous protocols before processing the redirect
isDangerousProtocol(redirect.options.href, this.protocolBlocklist)
isDangerousProtocol(redirect.options.href, this.protocolAllowlist)
) {
throw new Error(
`Redirect blocked: unsafe protocol in href "${redirect.options.href}". Blocked protocols: ${Array.from(this.protocolBlocklist).join(', ')}.`,
`Redirect blocked: unsafe protocol in href "${redirect.options.href}". Allowed protocols: ${Array.from(this.protocolAllowlist).join(', ')}.`,
)
}

Expand Down
51 changes: 16 additions & 35 deletions packages/router-core/src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -536,41 +536,22 @@ function decodeSegment(segment: string): string {
}

/**
* Default list of URL protocols to block in links, redirects, and navigation.
* These protocols can be used to execute arbitrary code, access local files,
* or interact with browser internals in ways that could be exploited.
* Default list of URL protocols to allow in links, redirects, and navigation.
* Any absolute URL protocol not in this list is treated as dangerous by default.
*/
export const DEFAULT_PROTOCOL_BLOCKLIST = [
// Script execution protocols - can run arbitrary code
'javascript:', // Executes JavaScript in the current context (XSS vector)
'vbscript:', // Executes VBScript in IE/legacy browsers

// Local file access - can read sensitive files from the user's system
'file:', // Access to local filesystem (e.g., file:///etc/passwd)

// Data embedding protocols - can be used for XSS or data exfiltration
'blob:', // References blob URLs, can bypass CSP in some cases
'data:', // Inline data URLs, commonly used for XSS attacks

// Browser internal protocols - can access browser configuration/internals
'about:', // Browser internals (about:blank is safe, but about:config in Firefox could be targeted)

// Platform-specific protocols - can access app resources or extensions
'ms-appx:', // Windows UWP app local resources
'ms-appx-web:', // Windows UWP web app resources
'ms-browser-extension:', // Windows browser extension protocol
'chrome-extension:', // Chrome extension protocol (could trigger extension actions)
'moz-extension:', // Firefox extension protocol

// Archive/resource protocols - potential path traversal or information disclosure
'jar:', // Java archive protocol (path traversal attacks in some contexts)
'view-source:', // Information disclosure (reveals page source code)
'resource:', // Firefox internal resources
'wyciwyg:', // Firefox "what you cache is what you get" protocol
export const DEFAULT_PROTOCOL_ALLOWLIST = [
// Standard web navigation
'http:',
'https:',

// Common browser-safe actions
'mailto:',
'tel:',
'sms:',
]

/**
* Check if a URL string uses a protocol that is in the blocklist.
* Check if a URL string uses a protocol that is not in the allowlist.
* Returns true for blocked protocols like javascript:, blob:, data:, etc.
*
* The URL constructor correctly normalizes:
Expand All @@ -581,20 +562,20 @@ export const DEFAULT_PROTOCOL_BLOCKLIST = [
* For relative URLs (no protocol), returns false (safe).
*
* @param url - The URL string to check
* @param blocklist - Set of protocols to block
* @returns true if the URL uses a blocked protocol
* @param allowlist - Set of protocols to allow
* @returns true if the URL uses a protocol that is not allowed
*/
export function isDangerousProtocol(
url: string,
blocklist: Set<string>,
allowlist: Set<string>,
): boolean {
if (!url) return false

try {
// Use the URL constructor - it correctly normalizes protocols
// per WHATWG URL spec, handling all bypass attempts automatically
const parsed = new URL(url)
return blocklist.has(parsed.protocol)
return !allowlist.has(parsed.protocol)
} catch {
// URL constructor throws for relative URLs (no protocol)
// These are safe - they can't execute scripts
Expand Down
Loading
Loading