stricter image checking

This commit is contained in:
Evert Prants 2024-06-09 12:00:15 +03:00
parent 46351fb17d
commit d8f6d24511
Signed by: evert
GPG Key ID: 1688DA83D222D0B5
5 changed files with 61 additions and 10 deletions

29
package-lock.json generated
View File

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

View File

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

View File

@ -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<boolean>;
export let url: string = '/account';
@ -99,7 +99,11 @@
{#if picker}
<FormControl>
<label for="avatar-file">{$t('account.avatar.uploadLabel')}</label>
<input type="file" on:change={(e) => readFile(e.target)} accept={allowedImages.join(',')} />
<input
type="file"
on:change={(e) => readFile(e.target)}
accept={ALLOWED_IMAGES.join(',')}
/>
<span>{$t('account.avatar.hint')}</span>
</FormControl>
{/if}

View File

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

View File

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