From d8f6d24511b758dcc4232bfb5d4eca77ace7b224 Mon Sep 17 00:00:00 2001 From: Evert Prants Date: Sun, 9 Jun 2024 12:00:15 +0300 Subject: [PATCH] stricter image checking --- package-lock.json | 29 +++++++++++++++++-- package.json | 1 + src/lib/components/avatar/AvatarModal.svelte | 8 ++++-- src/lib/constants.ts | 3 +- src/lib/server/upload.ts | 30 +++++++++++++++++--- 5 files changed, 61 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index 6be304d..f65a418 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,6 +13,7 @@ "cropperjs": "^1.6.2", "dotenv": "^16.4.5", "drizzle-orm": "^0.30.10", + "image-size": "^1.1.1", "jose": "^5.3.0", "mime-types": "^2.1.35", "mysql2": "^3.9.7", @@ -3622,6 +3623,20 @@ "node": ">= 4" } }, + "node_modules/image-size": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.1.1.tgz", + "integrity": "sha512-541xKlUw6jr/6gGuk92F+mYM5zaFAc5ahphvkqvNe2bQ6gVBkd6bfrmVJ2t4KDAfikAYZyIqTnktX3i6/aQDrQ==", + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/import-fresh": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", @@ -4075,9 +4090,9 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mysql2": { - "version": "3.9.7", - "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz", - "integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==", + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz", + "integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==", "dependencies": { "denque": "^2.1.0", "generate-function": "^2.3.1", @@ -4487,6 +4502,14 @@ "node": ">=10.13.0" } }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", diff --git a/package.json b/package.json index 564349b..fb82c5b 100644 --- a/package.json +++ b/package.json @@ -43,6 +43,7 @@ "cropperjs": "^1.6.2", "dotenv": "^16.4.5", "drizzle-orm": "^0.30.10", + "image-size": "^1.1.1", "jose": "^5.3.0", "mime-types": "^2.1.35", "mysql2": "^3.9.7", diff --git a/src/lib/components/avatar/AvatarModal.svelte b/src/lib/components/avatar/AvatarModal.svelte index f5ed925..e8ef394 100644 --- a/src/lib/components/avatar/AvatarModal.svelte +++ b/src/lib/components/avatar/AvatarModal.svelte @@ -7,7 +7,7 @@ import 'cropperjs/dist/cropper.css'; import { invalidateAll } from '$app/navigation'; import FormControl from '../form/FormControl.svelte'; - import { allowedImages } from '$lib/constants'; + import { ALLOWED_IMAGES } from '$lib/constants'; export let show: Writable; export let url: string = '/account'; @@ -99,7 +99,11 @@ {#if picker} - readFile(e.target)} accept={allowedImages.join(',')} /> + readFile(e.target)} + accept={ALLOWED_IMAGES.join(',')} + /> {$t('account.avatar.hint')} {/if} diff --git a/src/lib/constants.ts b/src/lib/constants.ts index d290e75..82bcf7c 100644 --- a/src/lib/constants.ts +++ b/src/lib/constants.ts @@ -1,3 +1,4 @@ -export const allowedImages = ['image/png', 'image/jpg', 'image/jpeg']; +export const ALLOWED_IMAGES = ['image/png', 'image/jpg', 'image/jpeg']; export const OAUTH2_MAX_REDIRECTS = 5; export const OAUTH2_MAX_URLS = 1; +export const MAX_FILE_SIZE_MB = 10; diff --git a/src/lib/server/upload.ts b/src/lib/server/upload.ts index c522610..8fab76e 100644 --- a/src/lib/server/upload.ts +++ b/src/lib/server/upload.ts @@ -13,6 +13,9 @@ import { readFile, stat, unlink, writeFile } from 'fs/promises'; import { join } from 'path'; import * as mime from 'mime-types'; import { OAuth2Clients } from './oauth2'; +import { MAX_FILE_SIZE_MB, ALLOWED_IMAGES } from '$lib/constants'; +import { error } from '@sveltejs/kit'; +import imageSize from 'image-size'; export class Uploads { static userFallbackImage: Buffer; @@ -115,12 +118,31 @@ export class Uploads { .where(eq(oauth2Client.id, client.id)); } + static async ensureAllowedFile(file: File) { + if (!ALLOWED_IMAGES.includes(file.type)) { + error(400, 'File is not an image'); + } + + if (file.size > MAX_FILE_SIZE_MB * 1e6) { + error(400, 'File too big'); + } + + const arrayBuffer = await file.arrayBuffer(); + const buffer = Buffer.from(arrayBuffer); + const result = imageSize(buffer); + if (!result?.height || !result?.width || result.height / result.width !== 1) { + error(400, 'Invalid image file'); + } + + return buffer; + } + static async saveAvatar(subject: User, file: File) { const ext = mime.extension(file.type); const newName = `user-${subject.uuid.split('-')[0]}-${Math.floor(Date.now() / 1000)}.${ext}`; - const arrayBuffer = await file.arrayBuffer(); + const buffer = await Uploads.ensureAllowedFile(file); // Write to filesystem - await writeFile(join(Uploads.uploads, newName), Buffer.from(arrayBuffer)); + await writeFile(join(Uploads.uploads, newName), buffer); // Remove old await Uploads.removeAvatar(subject); // Update DB @@ -139,9 +161,9 @@ export class Uploads { static async saveClientAvatar(client: OAuth2Client, uploader: User, file: File) { const ext = mime.extension(file.type); const newName = `client-${client.client_id.substring(0, 8)}-${Math.floor(Date.now() / 1000)}.${ext}`; - const arrayBuffer = await file.arrayBuffer(); + const buffer = await Uploads.ensureAllowedFile(file); // Write to filesystem - await writeFile(join(Uploads.uploads, newName), Buffer.from(arrayBuffer)); + await writeFile(join(Uploads.uploads, newName), buffer); // Remove old await Uploads.removeClientAvatar(client); // Update DB