add: simple validation and sanitizing

This commit is contained in:
Rasmus Krogh Udengaard
2026-03-05 15:04:12 +01:00
parent d046c66bc7
commit 9f8ae9a972
7 changed files with 160 additions and 14 deletions

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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, {

View File

@@ -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: {

View File

@@ -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

View File

@@ -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
});

View File

@@ -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;
}