add: simple validation and sanitizing
This commit is contained in:
@@ -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);
|
||||
|
||||
86
src/lib/server/validation.ts
Normal file
86
src/lib/server/validation.ts
Normal 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;
|
||||
}
|
||||
@@ -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, {
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
});
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user