Skip to content

Commit c63d7b8

Browse files
committed
sanitize next
1 parent 571f212 commit c63d7b8

File tree

3 files changed

+62
-4
lines changed

3 files changed

+62
-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: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
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+
});

app/platform-redirect/utils.ts

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

0 commit comments

Comments
 (0)