Skip to content

Commit f79bc07

Browse files
fix: url santization
1 parent 5e5ebc6 commit f79bc07

File tree

6 files changed

+44
-27
lines changed

6 files changed

+44
-27
lines changed

package-lock.json

Lines changed: 10 additions & 22 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
},
1616
"dependencies": {
1717
"@tanstack/react-query": "^5.54.1",
18-
"@vercel/blob": "^1.1.1",
18+
"@vercel/blob": "^2.0.1",
1919
"appwrite": "^15.0.0",
2020
"dotenv": "^17.2.1",
2121
"next": "^16.1.6",

server.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,15 @@
1+
/**
2+
* Custom Next.js server for local development only.
3+
*
4+
* SECURITY NOTE: This uses HTTP (not HTTPS) which is acceptable for localhost development.
5+
* In production, this file is NOT used - Vercel handles HTTPS termination automatically.
6+
* Do not use this server in production environments.
7+
*/
18
const { createServer } = require('http');
29
const { parse } = require('url');
310
const next = require('next');
411

12+
// This server is for LOCAL DEVELOPMENT ONLY - Vercel handles production
513
const dev = process.env.NODE_ENV !== 'production';
614
const hostname = 'localhost';
715
const port = 3000;

src/components/speakersList/speakersList.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Speaker } from '@/types';
55
import styles from './speakersList.module.css';
66
import Image from 'next/image';
77
import { LABELS } from '@/app/labels';
8+
import { sanitizeExternalUrl } from '@/utils/urlSanitizer';
89

910
interface SpeakersListProps {
1011
selectedTopic?: string | null;
@@ -153,9 +154,9 @@ export default function SpeakersList({ selectedTopic }: SpeakersListProps) {
153154
{formatDate(speaker.lastSpoke)}
154155
</p>
155156
)}
156-
{speaker.linkedin && (
157+
{sanitizeExternalUrl(speaker.linkedin) && (
157158
<a
158-
href={speaker.linkedin}
159+
href={sanitizeExternalUrl(speaker.linkedin)!}
159160
target='_blank'
160161
rel='noopener noreferrer'
161162
className={styles.linkedinLink}

src/components/teamList/teamList.tsx

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { Speaker } from '@/types';
55
import OptimizedImage from '@/components/ui/OptimizedImage';
66
import styles from './teamList.module.css';
77
import { LABELS } from '@/app/labels';
8+
import { sanitizeExternalUrl } from '@/utils/urlSanitizer';
89

910
function TeamListComponent({ peopleData }: { peopleData: Speaker[] }) {
1011
return (
@@ -35,9 +36,9 @@ function TeamListComponent({ peopleData }: { peopleData: Speaker[] }) {
3536
{LABELS.teamList.admin_role}
3637
</p>
3738
<p className={styles.teamCompany}>{LABELS.app.orgName}</p>
38-
{person.linkedin && (
39+
{sanitizeExternalUrl(person.linkedin) && (
3940
<a
40-
href={person.linkedin}
41+
href={sanitizeExternalUrl(person.linkedin)!}
4142
target='_blank'
4243
rel='noopener noreferrer'
4344
className={styles.linkedinLink}

src/utils/urlSanitizer.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
/**
2+
* Sanitize a generic external URL.
3+
* Only allows HTTPS URLs to prevent javascript: and data: XSS attacks.
4+
*
5+
* @param url - The URL to sanitize
6+
* @returns The sanitized URL if valid, null otherwise
7+
*/
8+
export function sanitizeExternalUrl(url: string | undefined): string | null {
9+
if (!url) return null;
10+
try {
11+
const parsed = new URL(url);
12+
if (parsed.protocol === 'https:') {
13+
return parsed.href;
14+
}
15+
return null;
16+
} catch {
17+
return null;
18+
}
19+
}

0 commit comments

Comments
 (0)