Express에서 Request Body 안전하게 받기

CloudJun·2022년 1월 20일
1

개요

Node에서는 유저가 보내주는 Requst 데이터를 검증하기 위해 class-validator를 사용한다.

class-validator는 Dto에 정의한 타입 정보를 기반으로 유저가 보내준 데이터를 class로 감싼 다음 타입에 맞는지 검증한다.

공식 홈페이지 예제에서는 특정 Dto class를 생성한다음 Dto 객체에 각각 req.body를 넣어주는 방식으로 되어있는데 각 컨트롤러마다 그렇게 넣어주고 싶지는 않았다..

class-transformer로 특정 Dto Class 객체를 바로 생성 할 수 있었는데, 이 경우 발생하는 문제를 공유하고 안전하게 받는 방법에 대해 정리했다.

공홈 Class-validator 예시

export class Post {
  @Length(10, 20)
  title: string;

  @Contains('hello')
  text: string;

  @IsInt()
  @Min(0)
  @Max(10)
  rating: number;

  @IsEmail()
  email: string;

  @IsFQDN()
  site: string;

  @IsDate()
  createDate: Date;
}

let post = new Post();
post.title = 'Hello'; // should not pass
post.text = 'this is a great post about hell world'; // should not pass
post.rating = 11; // should not pass
post.email = 'google.com'; // should not pass
post.site = 'googlecom'; // should not pass

validate(post).then(errors => {
  // errors is an array of validation errors
  if (errors.length > 0) {
    console.log('validation failed. errors: ', errors);
  } else {
    console.log('validation succeed');
  }
});

validateOrReject(post).catch(errors => {
  console.log('Promise rejected (validation failed). Errors: ', errors);
});
// or
async function validateOrRejectExample(input) {
  try {
    await validateOrReject(input);
  } catch (errors) {
    console.log('Caught promise rejection (validation failed). Errors: ', errors);
  }
}

나는 각각 api 컨트롤러에서 검증을 하기보다는 미들웨어로 만들고 검증을 하고 싶었기 때문에 이렇게 하고 싶지 않았다.

프로젝트 구조

Dto

import { IsNumber, IsString, Min } from 'class-validator';
import { Expose } from 'class-transformer';

export class TestDto {
  @Expose()
  @IsNumber()
  public userIdx: number;

  @Expose()
	@IsString()
  public userName: string;
}

검증 미들웨어

import { plainToClass } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';

export default function validationMiddleware(type: any, skipMissingProperties = false): RequestHandler {
  return (req, res, next) => {
    validate(plainToClass(type, req.body), { skipMissingProperties }).then((errors: ValidationError[]) => {
      req.body = plainToClass(type, req.body, { excludeExtraneousValues: true });

      if (errors.length > 0) {
        const message = errors.map((error: ValidationError) => Object.values(error.constraints || error.children[0].children)).join(', ');
        next(new HttpException(400, message));
      } else {
        next();
      }
    });
  };
}

이렇게 유저가 API에 데이터를 보내면 먼저 검증을 할 수 있는 미들웨어를 구성했는데,

여기서 plainToclass는 class-transformer 로 유저에게 날라오는 request 데이터를 Class로 객체로 전체를 바꿔준다.

다만, Dto에 선언하지 않은 데이터도 바꿔주는데 내 경우 이게 큰 문제로 다가왔다. Class 선언해서 한땀한땀 넣어줬으면 이런 문제는 없었겠지만..

문제가 되는 부분

{
	"userIdx": 1,
	"userName": "hello"
}

이렇게 날라올땐 문제가 없다

{
	"userIdx": 1,
	"userName": "hello",
	"test": "asdqwe123" // 이건 dto에 넣어두지 않았는데 객체속에 같이 넣어진다는 것
}

“test” 는 내가 dto에 선언하지 않았지만 class-transofrmer 에서 자동으로 추가시켜서 넘겨준다.

해결 방법

모든 DTO에 데코레이터로 Expose를 추가해주고 미들웨어 검증에서는 plainToClass에서 환경변수로 excludeExtraneousValues 를 넣어준다

DTO

import { IsNumber, IsString, Min } from 'class-validator';
import { Expose } from 'class-transformer';

export class TestDto {
  @Expose()
  @IsNumber()
  public userIdx: number;

  @Expose()
	@IsString()
  public userName: string;
}
import { plainToClass } from 'class-transformer';
import { validate, ValidationError } from 'class-validator';

export default function validationMiddleware(type: any, skipMissingProperties = false): RequestHandler {
  return (req, res, next) => {
    validate(plainToClass(type, req.body, {excludeExtraneousValues: true }), { skipMissingProperties }).then((errors: ValidationError[]) => { // 이 부분
      req.body = plainToClass(type, req.body, { excludeExtraneousValues: true }); // 이 부분

      if (errors.length > 0) {
        const message = errors.map((error: ValidationError) => Object.values(error.constraints || error.children[0].children)).join(', ');
        next(new HttpException(400, message));
      } else {
        next();
      }
    });
  };
}

아직 아쉬운 부분이 있지만 이렇게 하면 해결.

사실 더 쉬운 방법이 있다.

그런데 버그라서 사용 할 수 없음;

https://github.com/typestack/class-transformer/issues/740

profile
짧고 굵게 살아가는 백엔드 개발자

0개의 댓글