stricter image checking
This commit is contained in:
parent
46351fb17d
commit
d8f6d24511
29
package-lock.json
generated
29
package-lock.json
generated
@ -13,6 +13,7 @@
|
|||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.30.10",
|
"drizzle-orm": "^0.30.10",
|
||||||
|
"image-size": "^1.1.1",
|
||||||
"jose": "^5.3.0",
|
"jose": "^5.3.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.9.7",
|
"mysql2": "^3.9.7",
|
||||||
@ -3622,6 +3623,20 @@
|
|||||||
"node": ">= 4"
|
"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": {
|
"node_modules/import-fresh": {
|
||||||
"version": "3.3.0",
|
"version": "3.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz",
|
||||||
@ -4075,9 +4090,9 @@
|
|||||||
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
|
||||||
},
|
},
|
||||||
"node_modules/mysql2": {
|
"node_modules/mysql2": {
|
||||||
"version": "3.9.7",
|
"version": "3.10.0",
|
||||||
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.9.7.tgz",
|
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.10.0.tgz",
|
||||||
"integrity": "sha512-KnJT8vYRcNAZv73uf9zpXqNbvBG7DJrs+1nACsjZP1HMJ1TgXEy8wnNilXAn/5i57JizXKtrUtwDB7HxT9DDpw==",
|
"integrity": "sha512-qx0mfWYt1DpTPkw8mAcHW/OwqqyNqBLBHvY5IjN8+icIYTjt6znrgYJ+gxqNNRpVknb5Wc/gcCM4XjbCR0j5tw==",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"denque": "^2.1.0",
|
"denque": "^2.1.0",
|
||||||
"generate-function": "^2.3.1",
|
"generate-function": "^2.3.1",
|
||||||
@ -4487,6 +4502,14 @@
|
|||||||
"node": ">=10.13.0"
|
"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": {
|
"node_modules/queue-microtask": {
|
||||||
"version": "1.2.3",
|
"version": "1.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz",
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
"cropperjs": "^1.6.2",
|
"cropperjs": "^1.6.2",
|
||||||
"dotenv": "^16.4.5",
|
"dotenv": "^16.4.5",
|
||||||
"drizzle-orm": "^0.30.10",
|
"drizzle-orm": "^0.30.10",
|
||||||
|
"image-size": "^1.1.1",
|
||||||
"jose": "^5.3.0",
|
"jose": "^5.3.0",
|
||||||
"mime-types": "^2.1.35",
|
"mime-types": "^2.1.35",
|
||||||
"mysql2": "^3.9.7",
|
"mysql2": "^3.9.7",
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
import 'cropperjs/dist/cropper.css';
|
import 'cropperjs/dist/cropper.css';
|
||||||
import { invalidateAll } from '$app/navigation';
|
import { invalidateAll } from '$app/navigation';
|
||||||
import FormControl from '../form/FormControl.svelte';
|
import FormControl from '../form/FormControl.svelte';
|
||||||
import { allowedImages } from '$lib/constants';
|
import { ALLOWED_IMAGES } from '$lib/constants';
|
||||||
|
|
||||||
export let show: Writable<boolean>;
|
export let show: Writable<boolean>;
|
||||||
export let url: string = '/account';
|
export let url: string = '/account';
|
||||||
@ -99,7 +99,11 @@
|
|||||||
{#if picker}
|
{#if picker}
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<label for="avatar-file">{$t('account.avatar.uploadLabel')}</label>
|
<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>
|
<span>{$t('account.avatar.hint')}</span>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
{/if}
|
{/if}
|
||||||
|
@ -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_REDIRECTS = 5;
|
||||||
export const OAUTH2_MAX_URLS = 1;
|
export const OAUTH2_MAX_URLS = 1;
|
||||||
|
export const MAX_FILE_SIZE_MB = 10;
|
||||||
|
@ -13,6 +13,9 @@ import { readFile, stat, unlink, writeFile } from 'fs/promises';
|
|||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import * as mime from 'mime-types';
|
import * as mime from 'mime-types';
|
||||||
import { OAuth2Clients } from './oauth2';
|
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 {
|
export class Uploads {
|
||||||
static userFallbackImage: Buffer;
|
static userFallbackImage: Buffer;
|
||||||
@ -115,12 +118,31 @@ export class Uploads {
|
|||||||
.where(eq(oauth2Client.id, client.id));
|
.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) {
|
static async saveAvatar(subject: User, file: File) {
|
||||||
const ext = mime.extension(file.type);
|
const ext = mime.extension(file.type);
|
||||||
const newName = `user-${subject.uuid.split('-')[0]}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
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
|
// Write to filesystem
|
||||||
await writeFile(join(Uploads.uploads, newName), Buffer.from(arrayBuffer));
|
await writeFile(join(Uploads.uploads, newName), buffer);
|
||||||
// Remove old
|
// Remove old
|
||||||
await Uploads.removeAvatar(subject);
|
await Uploads.removeAvatar(subject);
|
||||||
// Update DB
|
// Update DB
|
||||||
@ -139,9 +161,9 @@ export class Uploads {
|
|||||||
static async saveClientAvatar(client: OAuth2Client, uploader: User, file: File) {
|
static async saveClientAvatar(client: OAuth2Client, uploader: User, file: File) {
|
||||||
const ext = mime.extension(file.type);
|
const ext = mime.extension(file.type);
|
||||||
const newName = `client-${client.client_id.substring(0, 8)}-${Math.floor(Date.now() / 1000)}.${ext}`;
|
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
|
// Write to filesystem
|
||||||
await writeFile(join(Uploads.uploads, newName), Buffer.from(arrayBuffer));
|
await writeFile(join(Uploads.uploads, newName), buffer);
|
||||||
// Remove old
|
// Remove old
|
||||||
await Uploads.removeClientAvatar(client);
|
await Uploads.removeClientAvatar(client);
|
||||||
// Update DB
|
// Update DB
|
||||||
|
Loading…
Reference in New Issue
Block a user