add: simple validation and sanitizing
This commit is contained in:
@@ -125,7 +125,7 @@ const authConfig: SvelteKitAuthConfig = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
secret: env.AUTH_SECRET,
|
secret: env.AUTH_SECRET,
|
||||||
trustHost: true
|
trustHost: env.AUTH_TRUST_HOST === 'true'
|
||||||
};
|
};
|
||||||
|
|
||||||
export const { handle, signIn, signOut } = SvelteKitAuth(authConfig);
|
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';
|
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 }) => {
|
export const GET: RequestHandler = async ({ url }) => {
|
||||||
const imageUrl = url.searchParams.get('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 });
|
return new Response('Image URL is required', { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isValidImageUrl(imageUrl)) {
|
||||||
|
return new Response('Invalid image URL', { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Fetch the image with proper headers to avoid blocking
|
// Fetch the image with proper headers to avoid blocking
|
||||||
const response = await fetch(imageUrl, {
|
const response = await fetch(imageUrl, {
|
||||||
|
|||||||
@@ -1,6 +1,22 @@
|
|||||||
import { json } from '@sveltejs/kit';
|
import { json } from '@sveltejs/kit';
|
||||||
import type { RequestHandler } from './$types';
|
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 }) => {
|
export const POST: RequestHandler = async ({ request }) => {
|
||||||
const { url } = await request.json();
|
const { url } = await request.json();
|
||||||
|
|
||||||
@@ -8,6 +24,10 @@ export const POST: RequestHandler = async ({ request }) => {
|
|||||||
return json({ error: 'URL is required' }, { status: 400 });
|
return json({ error: 'URL is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isValidUrl(url)) {
|
||||||
|
return json({ error: 'Invalid URL' }, { status: 400 });
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const response = await fetch(url, {
|
const response = await fetch(url, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
@@ -3,11 +3,24 @@ import type { RequestHandler } from './$types';
|
|||||||
import { db } from '$lib/server/db';
|
import { db } from '$lib/server/db';
|
||||||
import { wishlists } from '$lib/server/schema';
|
import { wishlists } from '$lib/server/schema';
|
||||||
import { createId } from '@paralleldrive/cuid2';
|
import { createId } from '@paralleldrive/cuid2';
|
||||||
|
import { sanitizeString, sanitizeColor } from '$lib/server/validation';
|
||||||
|
|
||||||
export const POST: RequestHandler = async ({ request, locals }) => {
|
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 });
|
return json({ error: 'Title is required' }, { status: 400 });
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,9 +33,9 @@ export const POST: RequestHandler = async ({ request, locals }) => {
|
|||||||
const [wishlist] = await db
|
const [wishlist] = await db
|
||||||
.insert(wishlists)
|
.insert(wishlists)
|
||||||
.values({
|
.values({
|
||||||
title: title.trim(),
|
title,
|
||||||
description: description?.trim() || null,
|
description,
|
||||||
color: color?.trim() || null,
|
color,
|
||||||
ownerToken,
|
ownerToken,
|
||||||
publicToken,
|
publicToken,
|
||||||
userId
|
userId
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import { users } from '$lib/server/schema';
|
|||||||
import { eq } from 'drizzle-orm';
|
import { eq } from 'drizzle-orm';
|
||||||
import bcrypt from 'bcrypt';
|
import bcrypt from 'bcrypt';
|
||||||
import { env } from '$env/dynamic/private';
|
import { env } from '$env/dynamic/private';
|
||||||
|
import { sanitizeString, sanitizeUsername } from '$lib/server/validation';
|
||||||
|
|
||||||
export const load: PageServerLoad = async () => {
|
export const load: PageServerLoad = async () => {
|
||||||
// Determine which OAuth providers are available
|
// Determine which OAuth providers are available
|
||||||
@@ -31,12 +32,18 @@ export const actions: Actions = {
|
|||||||
const password = formData.get('password') as string;
|
const password = formData.get('password') as string;
|
||||||
const confirmPassword = formData.get('confirmPassword') as string;
|
const confirmPassword = formData.get('confirmPassword') as string;
|
||||||
|
|
||||||
if (!name?.trim()) {
|
let sanitizedUsername: string;
|
||||||
return fail(400, { error: 'Name is required', name, username });
|
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()) {
|
if (!sanitizedName) {
|
||||||
return fail(400, { error: 'Username is required', name, username });
|
return fail(400, { error: 'Name is required', name, username });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!password || password.length < 8) {
|
if (!password || password.length < 8) {
|
||||||
@@ -48,7 +55,7 @@ export const actions: Actions = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const existingUser = await db.query.users.findFirst({
|
const existingUser = await db.query.users.findFirst({
|
||||||
where: eq(users.username, username.trim().toLowerCase())
|
where: eq(users.username, sanitizedUsername)
|
||||||
});
|
});
|
||||||
|
|
||||||
if (existingUser) {
|
if (existingUser) {
|
||||||
@@ -58,8 +65,8 @@ export const actions: Actions = {
|
|||||||
const hashedPassword = await bcrypt.hash(password, 10);
|
const hashedPassword = await bcrypt.hash(password, 10);
|
||||||
|
|
||||||
await db.insert(users).values({
|
await db.insert(users).values({
|
||||||
name: name.trim(),
|
name: sanitizedName,
|
||||||
username: username.trim().toLowerCase(),
|
username: sanitizedUsername,
|
||||||
password: hashedPassword
|
password: hashedPassword
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const load: PageServerLoad = async ({ params, locals }) => {
|
|||||||
)
|
)
|
||||||
});
|
});
|
||||||
isSaved = !!saved;
|
isSaved = !!saved;
|
||||||
isClaimed = !!saved?.ownerToken; // User has claimed if ownerToken exists in savedWishlists
|
isClaimed = !!saved?.ownerToken;
|
||||||
savedWishlistId = saved?.id || null;
|
savedWishlistId = saved?.id || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user