Skip to content

Commit 97b3537

Browse files
authored
fix(platform): Sanitize next in platform-redirect (#12047)
1 parent 648f05d commit 97b3537

File tree

3 files changed

+84
-4
lines changed

3 files changed

+84
-4
lines changed

app/platform-redirect/page.tsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {SmartLink} from 'sentry-docs/components/smartLink';
88
import {extractPlatforms, getDocsRootNode, nodeForPath} from 'sentry-docs/docTree';
99
import {setServerContext} from 'sentry-docs/serverContext';
1010

11+
import {sanitizeNext} from './utils';
12+
1113
export const metadata: Metadata = {
1214
robots: 'noindex',
1315
title: 'Platform Specific Content',
@@ -27,8 +29,7 @@ export default async function Page(props: {
2729
next = next[0];
2830
}
2931

30-
// discard the hash
31-
const [pathname, _] = next.split('#');
32+
const pathname = sanitizeNext(next);
3233
const rootNode = await getDocsRootNode();
3334
const defaultTitle = 'Platform Specific Content';
3435
let description = '';
@@ -64,7 +65,7 @@ export default async function Page(props: {
6465
p => p.key === requestedPlatform?.toLowerCase()
6566
);
6667
if (isValidPlatform) {
67-
return redirect(`/platforms/${requestedPlatform}${next}`);
68+
return redirect(`/platforms/${requestedPlatform}${pathname}`);
6869
}
6970
}
7071

@@ -83,7 +84,7 @@ export default async function Page(props: {
8384
<ul>
8485
{platformList.map(p => (
8586
<li key={p.key}>
86-
<SmartLink to={`/platforms/${p.key}${next}`}>
87+
<SmartLink to={`/platforms/${p.key}${pathname}`}>
8788
<PlatformIcon
8889
size={16}
8990
platform={p.icon ?? p.key}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import {describe, expect, it} from 'vitest';
2+
3+
import {sanitizeNext} from './utils';
4+
5+
describe('sanitizeNext', () => {
6+
it('should return an empty string for external URLs', () => {
7+
expect(sanitizeNext('http://example.com')).toBe('');
8+
expect(sanitizeNext('https://example.com')).toBe('');
9+
expect(sanitizeNext('//example.com')).toBe('');
10+
});
11+
12+
it('should prepend a slash if missing', () => {
13+
expect(sanitizeNext('path/to/resource')).toBe('/path/to/resource');
14+
});
15+
16+
it('should not modify a valid internal path', () => {
17+
expect(sanitizeNext('/path/to/resource')).toBe('/path/to/resource');
18+
});
19+
20+
it('should remove unsafe characters', () => {
21+
expect(sanitizeNext('/path/to/resource?query=1')).toBe('/path/to/resource');
22+
expect(sanitizeNext('/path/to/resource#hash')).toBe('/path/to/resource');
23+
});
24+
25+
it('should allow alphanumeric and hyphens', () => {
26+
expect(sanitizeNext('/path-to/resource123')).toBe('/path-to/resource123');
27+
});
28+
29+
it('should return an empty string for paths with colons', () => {
30+
expect(sanitizeNext('/path:to/resource')).toBe('');
31+
});
32+
33+
it('should return an empty string for the root path', () => {
34+
expect(sanitizeNext('/')).toBe('');
35+
});
36+
37+
it('should decode URL encoded characters', () => {
38+
expect(sanitizeNext('/path%2Fwith%2Fslashes')).toBe('/path/with/slashes');
39+
});
40+
41+
it('should return an empty string for a malformed URI component', () => {
42+
const input = '%E0%A4%A'; // Malformed URI
43+
const expectedOutput = '';
44+
expect(sanitizeNext(input)).toBe(expectedOutput);
45+
});
46+
});

app/platform-redirect/utils.ts

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
export const sanitizeNext = (next: string) => {
2+
// Safely decode URI component
3+
let sanitizedNext: string;
4+
try {
5+
sanitizedNext = decodeURIComponent(next);
6+
} catch (e) {
7+
// Return empty string if decoding fails
8+
return '';
9+
}
10+
11+
// Validate that next is an internal path
12+
if (
13+
sanitizedNext.startsWith('//') ||
14+
sanitizedNext.startsWith('http') ||
15+
sanitizedNext.includes(':')
16+
) {
17+
// Reject potentially malicious redirects
18+
sanitizedNext = '';
19+
}
20+
21+
// Ensure next starts with a forward slash and only contains safe characters
22+
if (sanitizedNext && !sanitizedNext.startsWith('/')) {
23+
sanitizedNext = '/' + sanitizedNext;
24+
}
25+
26+
// Discard hash and path parameters
27+
const [pathname] = sanitizedNext.split('#')[0].split('?');
28+
29+
// Only allow alphanumeric, hyphens
30+
sanitizedNext = pathname.replace(/[^\w\-\/]/g, '');
31+
32+
return sanitizedNext === '/' ? '' : sanitizedNext;
33+
};

0 commit comments

Comments
 (0)