diff --git a/src/auth.ts b/src/auth.ts index 411ece9..19b0e61 100644 --- a/src/auth.ts +++ b/src/auth.ts @@ -125,7 +125,7 @@ const authConfig: SvelteKitAuthConfig = { } }, secret: env.AUTH_SECRET, - trustHost: true + trustHost: env.AUTH_TRUST_HOST === 'true' }; export const { handle, signIn, signOut } = SvelteKitAuth(authConfig); diff --git a/src/lib/server/validation.ts b/src/lib/server/validation.ts new file mode 100644 index 0000000..18335af --- /dev/null +++ b/src/lib/server/validation.ts @@ -0,0 +1,86 @@ +export function sanitizeString(input: string | null | undefined, maxLength: number = 1000): string | null { + if (input === null || input === undefined) { + return null; + } + const trimmed = input.trim(); + if (trimmed.length > maxLength) { + throw new Error(`Input exceeds maximum length of ${maxLength}`); + } + return trimmed; +} + +export function sanitizeUrl(url: string | null | undefined): string | null { + if (!url) { + return null; + } + return url.trim(); +} + +export function sanitizeText(input: string | null | undefined, maxLength: number = 10000): string | null { + if (input === null || input === undefined) { + return null; + } + const trimmed = input.trim(); + if (trimmed.length > maxLength) { + throw new Error(`Text exceeds maximum length of ${maxLength}`); + } + return trimmed; +} + +export function sanitizePrice(price: string | null | undefined): number | null { + if (!price) { + return null; + } + const parsed = parseFloat(price.trim()); + if (isNaN(parsed)) { + throw new Error('Invalid price format'); + } + if (parsed < 0) { + throw new Error('Price cannot be negative'); + } + return parsed; +} + +export function sanitizeColor(color: string | null | undefined): string | null { + if (!color) { + return null; + } + return color.trim(); +} + +export function sanitizeCurrency(currency: string | null | undefined): string { + if (!currency) { + return 'DKK'; + } + return currency.trim().toUpperCase(); +} + +export function sanitizeUsername(username: string): string { + const trimmed = username.trim().toLowerCase(); + if (trimmed.length < 3 || trimmed.length > 50) { + throw new Error('Username must be between 3 and 50 characters'); + } + return trimmed; +} + +export function sanitizeId(id: string | null | undefined): string { + if (!id) { + throw new Error('ID is required'); + } + const trimmed = id.trim(); + if (trimmed.length === 0 || trimmed.length > 100) { + throw new Error('Invalid ID'); + } + return trimmed; +} + +export function sanitizeToken(token: string | null | undefined): string { + if (!token) { + throw new Error('Token is required'); + } + const trimmed = token.trim(); + if (trimmed.length === 0 || trimmed.length > 100) { + throw new Error('Invalid token'); + } + return trimmed; +} \ No newline at end of file diff --git a/src/routes/api/image-proxy/+server.ts b/src/routes/api/image-proxy/+server.ts index 6ce75f3..f15e262 100644 --- a/src/routes/api/image-proxy/+server.ts +++ b/src/routes/api/image-proxy/+server.ts @@ -1,5 +1,21 @@ import type { RequestHandler } from './$types'; +function isValidImageUrl(url: string): boolean { + try { + const parsedUrl = new URL(url); + if (!['http:', 'https:'].includes(parsedUrl.protocol)) { + return false; + } + const hostname = parsedUrl.hostname.toLowerCase(); + if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') { + return false; + } + return true; + } catch { + return false; + } +} + export const GET: RequestHandler = async ({ url }) => { const imageUrl = url.searchParams.get('url'); @@ -7,6 +23,10 @@ export const GET: RequestHandler = async ({ url }) => { return new Response('Image URL is required', { status: 400 }); } + if (!isValidImageUrl(imageUrl)) { + return new Response('Invalid image URL', { status: 400 }); + } + try { // Fetch the image with proper headers to avoid blocking const response = await fetch(imageUrl, { diff --git a/src/routes/api/scrape-images/+server.ts b/src/routes/api/scrape-images/+server.ts index a1400d3..a4515dd 100644 --- a/src/routes/api/scrape-images/+server.ts +++ b/src/routes/api/scrape-images/+server.ts @@ -1,6 +1,22 @@ import { json } from '@sveltejs/kit'; import type { RequestHandler } from './$types'; +function isValidUrl(urlString: string): boolean { + try { + const url = new URL(urlString); + if (!['http:', 'https:'].includes(url.protocol)) { + return false; + } + const hostname = url.hostname.toLowerCase(); + if (hostname === 'localhost' || hostname === '127.0.0.1' || hostname === '::1') { + return false; + } + return true; + } catch { + return false; + } +} + export const POST: RequestHandler = async ({ request }) => { const { url } = await request.json(); @@ -8,6 +24,10 @@ export const POST: RequestHandler = async ({ request }) => { return json({ error: 'URL is required' }, { status: 400 }); } + if (!isValidUrl(url)) { + return json({ error: 'Invalid URL' }, { status: 400 }); + } + try { const response = await fetch(url, { headers: { diff --git a/src/routes/api/wishlists/+server.ts b/src/routes/api/wishlists/+server.ts index e43bd16..c70e3a4 100644 --- a/src/routes/api/wishlists/+server.ts +++ b/src/routes/api/wishlists/+server.ts @@ -3,11 +3,24 @@ import type { RequestHandler } from './$types'; import { db } from '$lib/server/db'; import { wishlists } from '$lib/server/schema'; import { createId } from '@paralleldrive/cuid2'; +import { sanitizeString, sanitizeColor } from '$lib/server/validation'; export const POST: RequestHandler = async ({ request, locals }) => { - const { title, description, color } = await request.json(); + const body = await request.json(); - if (!title?.trim()) { + let title: string | null; + let description: string | null; + let color: string | null; + + try { + title = sanitizeString(body.title, 200); + description = sanitizeString(body.description, 2000); + color = sanitizeColor(body.color); + } catch (error) { + return json({ error: 'Invalid input' }, { status: 400 }); + } + + if (!title) { return json({ error: 'Title is required' }, { status: 400 }); } @@ -20,9 +33,9 @@ export const POST: RequestHandler = async ({ request, locals }) => { const [wishlist] = await db .insert(wishlists) .values({ - title: title.trim(), - description: description?.trim() || null, - color: color?.trim() || null, + title, + description, + color, ownerToken, publicToken, userId diff --git a/src/routes/signup/+page.server.ts b/src/routes/signup/+page.server.ts index 017eb8b..0533980 100644 --- a/src/routes/signup/+page.server.ts +++ b/src/routes/signup/+page.server.ts @@ -5,6 +5,7 @@ import { users } from '$lib/server/schema'; import { eq } from 'drizzle-orm'; import bcrypt from 'bcrypt'; import { env } from '$env/dynamic/private'; +import { sanitizeString, sanitizeUsername } from '$lib/server/validation'; export const load: PageServerLoad = async () => { // Determine which OAuth providers are available @@ -31,12 +32,18 @@ export const actions: Actions = { const password = formData.get('password') as string; const confirmPassword = formData.get('confirmPassword') as string; - if (!name?.trim()) { - return fail(400, { error: 'Name is required', name, username }); + let sanitizedUsername: string; + let sanitizedName: string | null; + + try { + sanitizedName = sanitizeString(name, 100); + sanitizedUsername = sanitizeUsername(username); + } catch (error) { + return fail(400, { error: 'Invalid input', name, username }); } - if (!username?.trim()) { - return fail(400, { error: 'Username is required', name, username }); + if (!sanitizedName) { + return fail(400, { error: 'Name is required', name, username }); } if (!password || password.length < 8) { @@ -48,7 +55,7 @@ export const actions: Actions = { } const existingUser = await db.query.users.findFirst({ - where: eq(users.username, username.trim().toLowerCase()) + where: eq(users.username, sanitizedUsername) }); if (existingUser) { @@ -58,8 +65,8 @@ export const actions: Actions = { const hashedPassword = await bcrypt.hash(password, 10); await db.insert(users).values({ - name: name.trim(), - username: username.trim().toLowerCase(), + name: sanitizedName, + username: sanitizedUsername, password: hashedPassword }); diff --git a/src/routes/wishlist/[token]/+page.server.ts b/src/routes/wishlist/[token]/+page.server.ts index 0c77ca2..b179547 100644 --- a/src/routes/wishlist/[token]/+page.server.ts +++ b/src/routes/wishlist/[token]/+page.server.ts @@ -34,7 +34,7 @@ export const load: PageServerLoad = async ({ params, locals }) => { ) }); isSaved = !!saved; - isClaimed = !!saved?.ownerToken; // User has claimed if ownerToken exists in savedWishlists + isClaimed = !!saved?.ownerToken; savedWishlistId = saved?.id || null; }