diff --git a/prisma/schema.prisma b/prisma/schema.prisma index e952f4d7..3a842179 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -9,7 +9,7 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["debian-openssl-3.0.x", "debian-openssl-1.1.x"] + binaryTargets = ["debian-openssl-3.0.x", "debian-openssl-1.1.x", "windows"] } datasource db { @@ -231,15 +231,21 @@ model SessionRefreshLog { // avatars.legacy.prisma // +enum AvatarType { + default + predefined + upload +} + model Avatar { id Int @id @default(autoincrement()) url String @db.VarChar name String @db.VarChar createdAt DateTime @default(now()) @map("created_at") @db.Timestamptz(6) - avatarType String @map("avatar_type") @db.VarChar + avatarType AvatarType @map("avatar_type") usageCount Int @default(0) @map("usage_count") - groupProfile GroupProfile[] - userProfile UserProfile[] + GroupProfile GroupProfile[] + UserProfile UserProfile[] @@map("avatar") } diff --git a/src/app.prisma b/src/app.prisma index 3a52b643..d4619433 100644 --- a/src/app.prisma +++ b/src/app.prisma @@ -1,6 +1,6 @@ generator client { provider = "prisma-client-js" - binaryTargets = ["debian-openssl-3.0.x", "debian-openssl-1.1.x"] + binaryTargets = ["debian-openssl-3.0.x", "debian-openssl-1.1.x", "windows"] } datasource db { diff --git a/src/avatars/avatars.error.ts b/src/avatars/avatars.error.ts index 618c578c..f96346a8 100644 --- a/src/avatars/avatars.error.ts +++ b/src/avatars/avatars.error.ts @@ -21,3 +21,9 @@ export class InvalidAvatarTypeError extends BaseError { super('InvalidAvatarTypeError', `Invalid Avatar type: ${avatarType}`, 400); } } + +export class InvalidPathError extends BaseError { + constructor() { + super('InvalidPathError', `Invalid path`, 400); + } +} diff --git a/src/avatars/avatars.module.ts b/src/avatars/avatars.module.ts index bb3a0f8c..bb27ee0f 100644 --- a/src/avatars/avatars.module.ts +++ b/src/avatars/avatars.module.ts @@ -1,14 +1,59 @@ import { Module } from '@nestjs/common'; import { MulterModule } from '@nestjs/platform-express'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { existsSync, mkdirSync } from 'fs'; import { diskStorage } from 'multer'; -import { extname, join } from 'path'; +import * as path from 'path'; +import { extname } from 'path'; import { v4 as uuidv4 } from 'uuid'; import { AuthModule } from '../auth/auth.module'; import { AvatarsController } from './avatars.controller'; +import { InvalidPathError } from './avatars.error'; import { Avatar } from './avatars.legacy.entity'; import { AvatarsService } from './avatars.service'; -import { existsSync, mkdirSync } from 'fs'; +declare module 'path' { + interface PlatformPath { + joinSafe(...paths: string[]): string | undefined; + } +} +function pathJoinSafePosix( + dir: string, + ...paths: string[] +): string | undefined { + dir = path.posix.normalize(dir); + const pathname = path.posix.join(dir, ...paths); + if (pathname.substring(0, dir.length) !== dir) return undefined; + return pathname; +} + +function pathJoinSafeWin32( + dir: string, + ...paths: string[] +): string | undefined { + dir = path.win32.normalize(dir); + const pathname = path.win32.join(dir, ...paths); + if (pathname.substring(0, dir.length) !== dir) return undefined; + return pathname; +} + +path.posix.joinSafe = pathJoinSafePosix; +path.win32.joinSafe = pathJoinSafeWin32; + +export function pathJoinSafe( + dir: string, + ...paths: string[] +): string | undefined { + dir = path.normalize(dir); + let joinedPath: string; + if (path.sep === '/') { + joinedPath = path.posix.join(dir, ...paths); + } else { + joinedPath = path.win32.join(dir, ...paths); + } + + if (joinedPath.substring(0, dir.length) !== dir) return undefined; + return joinedPath; +} @Module({ imports: [ TypeOrmModule.forFeature([Avatar]), @@ -22,7 +67,10 @@ import { existsSync, mkdirSync } from 'fs'; 'error', ); } - const dest = join(process.env.FILE_UPLOAD_PATH, 'avatars'); + const dest = pathJoinSafe(process.env.FILE_UPLOAD_PATH, 'avatars'); + if (!dest) { + throw new InvalidPathError(); + } if (!existsSync(dest)) { mkdirSync(dest, { recursive: true }); } diff --git a/src/avatars/avatars.service.ts b/src/avatars/avatars.service.ts index af2cf196..69305e99 100644 --- a/src/avatars/avatars.service.ts +++ b/src/avatars/avatars.service.ts @@ -1,10 +1,10 @@ import { Injectable, OnModuleInit } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; import { readdirSync } from 'fs'; -import { join } from 'path'; import { Repository } from 'typeorm'; -import { AvatarNotFoundError } from './avatars.error'; +import { AvatarNotFoundError, InvalidPathError } from './avatars.error'; import { Avatar, AvatarType } from './avatars.legacy.entity'; +import { pathJoinSafe } from './avatars.module'; @Injectable() export class AvatarsService implements OnModuleInit { constructor( @@ -15,8 +15,10 @@ export class AvatarsService implements OnModuleInit { this.initialize(); } private async initialize(): Promise { - const sourcePath = join(__dirname, '../resources/avatars'); - + const sourcePath = pathJoinSafe(__dirname, '../avatars'); + if (!sourcePath) { + throw new InvalidPathError(); + } const avatarFiles = readdirSync(sourcePath); /* istanbul ignore if */ if (!process.env.DEFAULT_AVATAR_NAME) { @@ -26,8 +28,10 @@ export class AvatarsService implements OnModuleInit { } const defaultAvatarName = process.env.DEFAULT_AVATAR_NAME; - const defaultAvatarPath = join(sourcePath, defaultAvatarName); - + const defaultAvatarPath = pathJoinSafe(sourcePath, defaultAvatarName); + if (!defaultAvatarPath) { + throw new InvalidPathError(); + } let defaultAvatar = await this.avatarRepository.findOneBy({ avatarType: AvatarType.default, }); @@ -55,7 +59,7 @@ export class AvatarsService implements OnModuleInit { } await Promise.all( predefinedAvatars.map(async (name) => { - const avatarPath = join(sourcePath, name); + const avatarPath = pathJoinSafe(sourcePath, name); const predefinedAvatar = this.avatarRepository.create({ url: avatarPath, name,