🐺

NestJSでレスポンスのフィールド余剰公開を防ぐために

2023/08/23に公開

はじめに

こんにちは、calloc134です。
皆さんは、APIを作成するときにどのようなレスポンスを返しますか?

例えば、ユーザー情報を返すAPIを作成するとき、以下のようなレスポンスを返すことが多いと思います。

{
  "id": 1,
  "name": "calloc134",
}

このAPIは特に問題はないですね。
しかし、このようなレスポンスを返すAPIはどうでしょうか。

{
  "id": 1,
  "name": "calloc134",
  "password": "$argon2i$v=19$m=16,t=2,p=1$MTIzNDU2Nzg$ZZ/eAcHRleU4ChG0EJ+2Mw",
  "createdAt": "2021-06-03T14:00:00.000Z",
  "updatedAt": "2021-06-03T14:00:00.000Z"
  ...
}

おそらく、このAPIはデータベースの情報をそのまま返しているのだと思います。
しかし、このAPIはセキュリティ上の問題があります。
特にpasswordフィールドはパスワードのハッシュであり、公開してはいけません。

このようになるのは避けたいですよね・・・。

NestJSでオブジェクトのフィールドをフィルタリングする

NestJSのServiceで、以下のようにユーザを返却していたとします。

import { Controller, Get } from '@nestjs/common';
import { PrismaService } from 'src/prisma.module';

@Injecable()
export class UserService {
  constructor(private readonly prisma: PrismaService) {}

  async findOne(id: number): Promise<User> {
    return this.prisma.user.findUniqueOrThrow({
      where: { id },
    });
  }
}

しかし、これだとpasswordフィールドが含まれてしまいます。

これをフィルタリングするために、class-transformerというライブラリを使います。

npm install class-transformer

次に、ユーザレスポンスとしての型を定義します。

import { Expose } from 'class-transformer';

export class UserResponse {
  @Expose()
  id: number;

  @Expose()
  name: string;

  constructor(partial: Partial<LimitedUserResDto>) {
    Object.assign(this, partial);
  }
}

@Expose()デコレータをつけることで、そのフィールドが公開されるようになります。

また、UserServiceで以下のようにUserResponseをnewして返却します。

import { Injecable } from '@nestjs/common';
import { PrismaService } from 'src/prisma.module';

@Injecable()
export class UserService {
  constructor(private readonly prisma: PrismaService) {}

  async findOne(id: number): Promise<UserResponse> {
    const user = await this.prisma.user.findUniqueOrThrow({
      where: { id },
    });
    return new UserResponse(user);
  }
}

最後に、main.tsにおいて、以下のようにしてシリアライザを設定します。

import { ClassSerializerInterceptor, ... } from '@nestjs/common';

async function bootstrap() {
  // ...
  const app = await NestFactory.create(AppModule);

  (...)

  // シリアライザを有効化
  app.useGlobalInterceptors(
    new ClassSerializerInterceptor(app.get(Reflector), {
      excludeExtraneousValues: true,
    })
  )

  // ...
  await app.listen(3000);
}
bootstrap();

ここで、excludeExtraneousValuestrueにすることで、@Expose()デコレータがついていないフィールドを除外することができます。

これで、passwordフィールドが含まれないレスポンスを返すことができました。

まとめ

お役に立てば幸いです。

GitHubで編集を提案

Discussion