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
9 changes: 5 additions & 4 deletions app/platform-redirect/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import {SmartLink} from 'sentry-docs/components/smartLink';
import {extractPlatforms, getDocsRootNode, nodeForPath} from 'sentry-docs/docTree';
import {setServerContext} from 'sentry-docs/serverContext';

import {sanitizeNext} from './utils';

export const metadata: Metadata = {
robots: 'noindex',
title: 'Platform Specific Content',
Expand All @@ -27,8 +29,7 @@ export default async function Page(props: {
next = next[0];
}

// discard the hash
const [pathname, _] = next.split('#');
const pathname = sanitizeNext(next);
const rootNode = await getDocsRootNode();
const defaultTitle = 'Platform Specific Content';
let description = '';
Expand Down Expand Up @@ -64,7 +65,7 @@ export default async function Page(props: {
p => p.key === requestedPlatform?.toLowerCase()
);
if (isValidPlatform) {
return redirect(`/platforms/${requestedPlatform}${next}`);
return redirect(`/platforms/${requestedPlatform}${pathname}`);
}
}

Expand All @@ -83,7 +84,7 @@ export default async function Page(props: {
<ul>
{platformList.map(p => (
<li key={p.key}>
<SmartLink to={`/platforms/${p.key}${next}`}>
<SmartLink to={`/platforms/${p.key}${pathname}`}>
<PlatformIcon
size={16}
platform={p.icon ?? p.key}
Expand Down
46 changes: 46 additions & 0 deletions app/platform-redirect/utils.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import {describe, expect, it} from 'vitest';

import {sanitizeNext} from './utils';

describe('sanitizeNext', () => {
it('should return an empty string for external URLs', () => {
expect(sanitizeNext('http://example.com')).toBe('');
expect(sanitizeNext('https://example.com')).toBe('');
expect(sanitizeNext('//example.com')).toBe('');
});

it('should prepend a slash if missing', () => {
expect(sanitizeNext('path/to/resource')).toBe('/path/to/resource');
});

it('should not modify a valid internal path', () => {
expect(sanitizeNext('/path/to/resource')).toBe('/path/to/resource');
});

it('should remove unsafe characters', () => {
expect(sanitizeNext('/path/to/resource?query=1')).toBe('/path/to/resource');
expect(sanitizeNext('/path/to/resource#hash')).toBe('/path/to/resource');
});

it('should allow alphanumeric and hyphens', () => {
expect(sanitizeNext('/path-to/resource123')).toBe('/path-to/resource123');
});

it('should return an empty string for paths with colons', () => {
expect(sanitizeNext('/path:to/resource')).toBe('');
});

it('should return an empty string for the root path', () => {
expect(sanitizeNext('/')).toBe('');
});

it('should decode URL encoded characters', () => {
expect(sanitizeNext('/path%2Fwith%2Fslashes')).toBe('/path/with/slashes');
});

it('should return an empty string for a malformed URI component', () => {
const input = '%E0%A4%A'; // Malformed URI
const expectedOutput = '';
expect(sanitizeNext(input)).toBe(expectedOutput);
});
});
33 changes: 33 additions & 0 deletions app/platform-redirect/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
export const sanitizeNext = (next: string) => {
// Safely decode URI component
let sanitizedNext: string;
try {
sanitizedNext = decodeURIComponent(next);
} catch (e) {
// Return empty string if decoding fails
return '';
}

// Validate that next is an internal path
if (
sanitizedNext.startsWith('//') ||
sanitizedNext.startsWith('http') ||
sanitizedNext.includes(':')
) {
// Reject potentially malicious redirects
sanitizedNext = '';
}

// Ensure next starts with a forward slash and only contains safe characters
if (sanitizedNext && !sanitizedNext.startsWith('/')) {
sanitizedNext = '/' + sanitizedNext;
}

// Discard hash and path parameters
const [pathname] = sanitizedNext.split('#')[0].split('?');

// Only allow alphanumeric, hyphens
sanitizedNext = pathname.replace(/[^\w\-\/]/g, '');

return sanitizedNext === '/' ? '' : sanitizedNext;
};
Loading