Skip to content

Commit 54f2a37

Browse files
committed
Replace ParamFerry component with safer ParamFerryLink component
1 parent 3305ecc commit 54f2a37

File tree

5 files changed

+62
-248
lines changed

5 files changed

+62
-248
lines changed

URL_PARAMETER_FERRYING.md

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -38,23 +38,19 @@ The parameter syncing patterns in `src/utils.ts` have been updated to focus on:
3838
- Navigation links now automatically include ferried parameters
3939
- Maintains the existing Button styling and behavior
4040

41-
### 4. Automatic Parameter Ferrying Component
41+
### 4. Safe Parameter Ferrying Link Component
4242

43-
**ParamFerry Component** (`src/components/paramFerry.tsx`):
44-
- A client-side component that automatically processes all links on the page
45-
- Uses MutationObserver to handle dynamically added links
46-
- Skips external links, anchors, mailto, and tel links
47-
- Marks processed links to avoid duplicate processing
48-
49-
**Added to Layout** (`app/layout.tsx`):
50-
- The ParamFerry component is included in the root layout
51-
- Works automatically across all pages without additional setup
43+
**ParamFerryLink Component** (`src/components/paramFerryLink.tsx`):
44+
- A safe Link component that handles parameter ferrying at the component level
45+
- No DOM manipulation - parameters are ferried during rendering
46+
- Only processes internal links (starting with `/` or `#`)
47+
- Can be used as a drop-in replacement for Next.js Link component
5248

5349
## How It Works
5450

55-
1. **Page Load**: When a page loads with URL parameters matching the patterns, the ParamFerry component identifies them
56-
2. **Link Processing**: All internal links on the page are processed to include the relevant parameters
57-
3. **Dynamic Updates**: New links added to the page (via JavaScript) are automatically processed
51+
1. **Component Rendering**: When link components render, they check for matching URL parameters in the current page
52+
2. **Parameter Extraction**: Parameters matching the specified patterns are extracted from the current URL
53+
3. **URL Construction**: Parameters are safely appended to internal link URLs during component rendering
5854
4. **Navigation**: When users click internal links, they carry forward the tracked parameters
5955

6056
## Examples
@@ -99,14 +95,22 @@ import {NavLink} from 'sentry-docs/components/navlink';
9995
<NavLink href="/docs/guides/">Guides</NavLink>
10096
```
10197

98+
### Using ParamFerryLink
99+
```tsx
100+
import {ParamFerryLink} from 'sentry-docs/components/paramFerryLink';
101+
102+
// Safe component-level parameter ferrying
103+
<ParamFerryLink href="/docs/getting-started/">Get Started</ParamFerryLink>
104+
```
105+
102106
## Security Features
103107

104108
The implementation includes comprehensive security measures:
105-
- **URL Scheme Validation**: Blocks dangerous URL schemes (`javascript:`, `data:`, `vbscript:`, `file:`, `about:`)
106-
- **Parameter Sanitization**: Sanitizes parameter keys and values to prevent injection attacks
107-
- **Length Limits**: Parameter values are limited to 500 characters
108-
- **Control Character Filtering**: Removes control characters from parameters
109-
- **Multiple Validation Layers**: URLs are validated at multiple stages of processing
109+
- **Same-Origin Policy**: Only processes URLs from the same origin to prevent cross-site attacks
110+
- **Internal Links Only**: Component-level ferrying only applies to internal links (starting with `/` or `#`)
111+
- **No DOM Manipulation**: Avoids security risks associated with modifying existing DOM elements
112+
- **Parameter Length Limits**: Parameter values are limited to 200 characters
113+
- **Type Validation**: Ensures all parameters are strings before processing
110114

111115
## Browser Compatibility
112116

app/layout.tsx

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import Script from 'next/script';
77
import PlausibleProvider from 'next-plausible';
88

99
import {ThemeProvider} from 'sentry-docs/components/theme-provider';
10-
import ParamFerry from 'sentry-docs/components/paramFerry';
1110

1211
const rubik = Rubik({
1312
weight: ['400', '500', '700'],
@@ -45,7 +44,6 @@ export default function RootLayout({children}: {children: React.ReactNode}) {
4544
>
4645
<Theme accentColor="iris" grayColor="sand" radius="large" scaling="95%">
4746
{children}
48-
<ParamFerry />
4947
</Theme>
5048
</ThemeProvider>
5149
<Script

src/components/paramFerry.tsx

Lines changed: 0 additions & 185 deletions
This file was deleted.

src/components/paramFerryLink.tsx

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
'use client';
2+
3+
import Link from 'next/link';
4+
import {ferryUrlParams} from 'sentry-docs/utils';
5+
6+
interface ParamFerryLinkProps {
7+
href: string;
8+
children: React.ReactNode;
9+
className?: string;
10+
onClick?: (e: React.MouseEvent) => void;
11+
target?: string;
12+
title?: string;
13+
[key: string]: any;
14+
}
15+
16+
/**
17+
* A Link component that automatically ferries URL parameters
18+
* This is a safer alternative to DOM manipulation
19+
*/
20+
export function ParamFerryLink({href, children, ...props}: ParamFerryLinkProps) {
21+
// Only ferry parameters for internal links
22+
const shouldFerry = href && (href.startsWith('/') || href.startsWith('#'));
23+
const finalHref = shouldFerry ? ferryUrlParams(href) : href;
24+
25+
return (
26+
<Link href={finalHref} {...props}>
27+
{children}
28+
</Link>
29+
);
30+
}
31+
32+
export default ParamFerryLink;

src/utils.ts

Lines changed: 8 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -91,28 +91,6 @@ export const marketingUrlParams = (): URLQueryObject => {
9191
return marketingParams;
9292
};
9393

94-
/**
95-
* Validate URL is safe to process (blocks dangerous schemes)
96-
* @param url - The URL to validate
97-
* @returns true if URL is safe, false otherwise
98-
*/
99-
const isSafeUrl = (url: string): boolean => {
100-
// Block dangerous schemes
101-
const dangerousSchemes = ['javascript:', 'data:', 'vbscript:', 'file:', 'about:'];
102-
const lowerUrl = url.toLowerCase().trim();
103-
104-
if (dangerousSchemes.some(scheme => lowerUrl.startsWith(scheme))) {
105-
return false;
106-
}
107-
108-
// Only allow http, https, and relative URLs
109-
if (lowerUrl.startsWith('http://') || lowerUrl.startsWith('https://') || lowerUrl.startsWith('/') || !lowerUrl.includes(':')) {
110-
return true;
111-
}
112-
113-
return false;
114-
};
115-
11694
/**
11795
* Ferry URL parameters from current page to target URL
11896
* @param targetUrl - The URL to append parameters to
@@ -124,8 +102,8 @@ export const ferryUrlParams = (targetUrl: string, additionalParams: URLQueryObje
124102
return targetUrl;
125103
}
126104

127-
// Validate URL safety first
128-
if (!isSafeUrl(targetUrl)) {
105+
// Only process relative URLs and same-origin URLs for security
106+
if (targetUrl.includes('://') && !targetUrl.startsWith(window.location.origin)) {
129107
return targetUrl;
130108
}
131109

@@ -139,32 +117,19 @@ export const ferryUrlParams = (targetUrl: string, additionalParams: URLQueryObje
139117
try {
140118
const url = new URL(targetUrl, window.location.origin);
141119

142-
// Double-check the constructed URL is safe
143-
if (!isSafeUrl(url.toString())) {
120+
// Only add parameters to same-origin URLs
121+
if (url.origin !== window.location.origin) {
144122
return targetUrl;
145123
}
146124

147-
// Add parameters to the URL with validation
125+
// Add parameters to the URL
148126
Object.entries(allParams).forEach(([key, value]) => {
149-
if (value && typeof value === 'string') {
150-
// Sanitize parameter key and value
151-
const sanitizedKey = key.replace(/[^\w\-._~]/g, '');
152-
const sanitizedValue = value.replace(/[\r\n\t]/g, '').substring(0, 500); // Limit length and remove control characters
153-
154-
if (sanitizedKey && sanitizedValue) {
155-
url.searchParams.set(sanitizedKey, sanitizedValue);
156-
}
127+
if (value && typeof value === 'string' && key && value.length < 200) {
128+
url.searchParams.set(key, value);
157129
}
158130
});
159131

160-
const result = url.toString();
161-
162-
// Final safety check
163-
if (!isSafeUrl(result)) {
164-
return targetUrl;
165-
}
166-
167-
return result;
132+
return url.toString();
168133
} catch (error) {
169134
console.warn('Error ferrying parameters:', error);
170135
return targetUrl;

0 commit comments

Comments
 (0)