From 85bd389a2bb4b09676c45ac59ac88af8f56dec0b Mon Sep 17 00:00:00 2001 From: rubikscraft Date: Sat, 26 Feb 2022 18:16:28 +0100 Subject: [PATCH] Fix bugs in data validation --- .../src/collections/imagedb/imagedb.service.ts | 9 +++++++-- backend/src/collections/userdb/userdb.service.ts | 11 ++++++++--- backend/src/routes/api/auth/auth.service.ts | 16 +++++++++++++--- backend/src/routes/api/auth/jwt.strategy.ts | 5 ++++- shared/src/entities/image.entity.ts | 15 ++++++++++----- shared/src/entities/user.entity.ts | 16 +++++++--------- 6 files changed, 49 insertions(+), 23 deletions(-) diff --git a/backend/src/collections/imagedb/imagedb.service.ts b/backend/src/collections/imagedb/imagedb.service.ts index f3c92d6..953057f 100644 --- a/backend/src/collections/imagedb/imagedb.service.ts +++ b/backend/src/collections/imagedb/imagedb.service.ts @@ -11,6 +11,7 @@ import { import { SupportedMime } from 'imagur-shared/dist/dto/mimes.dto'; import { GetCols } from '../collectionutils'; import { EImage } from 'imagur-shared/dist/entities/image.entity'; +import { plainToClass } from 'class-transformer'; @Injectable() export class ImageDBService { @@ -27,15 +28,19 @@ export class ImageDBService { const find = await this.findOne(hash); if (HasSuccess(find)) return find; - const imageEntity = new EImage(); + let imageEntity = new EImage(); imageEntity.data = image; imageEntity.mime = type; imageEntity.hash = hash; + try { - return await this.imageRepository.save(imageEntity); + imageEntity = await this.imageRepository.save(imageEntity); } catch (e: any) { return Fail(e?.message); } + + // Strips unwanted data + return plainToClass(EImage, imageEntity); } public async findOne( diff --git a/backend/src/collections/userdb/userdb.service.ts b/backend/src/collections/userdb/userdb.service.ts index 382d51b..895bee4 100644 --- a/backend/src/collections/userdb/userdb.service.ts +++ b/backend/src/collections/userdb/userdb.service.ts @@ -1,5 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; +import { plainToClass } from 'class-transformer'; import { validate } from 'class-validator'; import { EUser } from 'imagur-shared/dist/entities/user.entity'; import { @@ -26,17 +27,20 @@ export class UsersService { ): AsyncFailable { if (await this.exists(username)) return Fail('User already exists'); - const user = new EUser(); + let user = new EUser(); user.username = username; user.password = hashedPassword; try { - return await this.usersRepository.save(user); + user = await this.usersRepository.save(user, { reload: true }); } catch (e: any) { return Fail(e?.message); } + + return plainToClass(EUser, user); // Strips unwanted data } + // Returns user object without id public async delete(user: string | EUser): AsyncFailable { const userToModify = await this.resolve(user); if (HasFailed(userToModify)) return userToModify; @@ -94,7 +98,8 @@ export class UsersService { if (typeof user === 'string') { return await this.findOne(user); } else { - const errors = await validate(user); + user = plainToClass(EUser, user); + const errors = await validate(user, { forbidUnknownValues: true }); if (errors.length > 0) { this.logger.warn(errors); return Fail('Invalid user'); diff --git a/backend/src/routes/api/auth/auth.service.ts b/backend/src/routes/api/auth/auth.service.ts index 6bc9569..ce735ee 100644 --- a/backend/src/routes/api/auth/auth.service.ts +++ b/backend/src/routes/api/auth/auth.service.ts @@ -1,6 +1,8 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, Logger } from '@nestjs/common'; import { JwtService } from '@nestjs/jwt'; import * as bcrypt from 'bcrypt'; +import { plainToClass } from 'class-transformer'; +import { validate } from 'class-validator'; import { JwtDataDto } from 'imagur-shared/dist/dto/auth.dto'; import { EUser } from 'imagur-shared/dist/entities/user.entity'; import { AsyncFailable, HasFailed, Fail } from 'imagur-shared/dist/types'; @@ -8,6 +10,8 @@ import { UsersService } from '../../../collections/userdb/userdb.service'; @Injectable() export class AuthService { + private readonly logger = new Logger('AuthService'); + constructor( private usersService: UsersService, private jwtService: JwtService, @@ -37,9 +41,15 @@ export class AuthService { } async createToken(user: EUser): Promise { - const jwtData: JwtDataDto = { + const jwtData: JwtDataDto = plainToClass(JwtDataDto, { user, - }; + }); + + const errors = await validate(jwtData, { forbidUnknownValues: true }); + if (errors.length > 0) { + this.logger.warn(errors); + throw new Error('Invalid jwt token generated'); + } return this.jwtService.signAsync(jwtData); } diff --git a/backend/src/routes/api/auth/jwt.strategy.ts b/backend/src/routes/api/auth/jwt.strategy.ts index f78b042..b5c2434 100644 --- a/backend/src/routes/api/auth/jwt.strategy.ts +++ b/backend/src/routes/api/auth/jwt.strategy.ts @@ -22,7 +22,10 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') { async validate(payload: any): Promise { const jwt = plainToClass(JwtDataDto, payload); - const errors = await validate(jwt, { forbidUnknownValues: true }); + const errors = await validate(jwt, { + forbidUnknownValues: true, + }); + if (errors.length > 0) { this.logger.warn(errors); throw new UnauthorizedException(); diff --git a/shared/src/entities/image.entity.ts b/shared/src/entities/image.entity.ts index 5e21cdf..e4bf7af 100644 --- a/shared/src/entities/image.entity.ts +++ b/shared/src/entities/image.entity.ts @@ -1,23 +1,28 @@ -import { IsDefined, IsEnum, IsHash, IsNumber, IsOptional, IsString } from 'class-validator'; +import { Exclude } from 'class-transformer'; +import { + IsDefined, + IsEnum, + IsHash, + IsOptional, +} from 'class-validator'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { SupportedMime, SupportedMimes } from '../dto/mimes.dto'; @Entity() export class EImage { @PrimaryGeneratedColumn() - @IsNumber() - @IsDefined() - id: number; + @IsOptional() + id?: number; @Index() @Column({ unique: true }) - @IsString() @IsHash('sha256') hash: string; // Binary data @Column({ type: 'bytea', nullable: false, select: false }) @IsOptional() + @Exclude() data?: Buffer; @Column({ enum: SupportedMimes }) diff --git a/shared/src/entities/user.entity.ts b/shared/src/entities/user.entity.ts index c3addc8..95597e1 100644 --- a/shared/src/entities/user.entity.ts +++ b/shared/src/entities/user.entity.ts @@ -1,33 +1,31 @@ +import { Exclude, Expose } from 'class-transformer'; import { - IsBoolean, IsDefined, + IsEmpty, IsNotEmpty, - IsNumber, IsOptional, - IsString, } from 'class-validator'; import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; +// Different data for public and private + @Entity() export class EUser { @PrimaryGeneratedColumn() - @IsNumber() - @IsDefined() - id: number; + @IsOptional() + id?: number; @Index() @Column({ unique: true }) - @IsString() @IsNotEmpty() username: string; @Column({ default: false }) @IsDefined() - @IsBoolean() isAdmin: boolean; @Column({ select: false }) @IsOptional() - @IsString() + @Exclude() password?: string; }