본문으로 건너뛰기
개발 머꼬
개발 노트NestJS
hohyeon.dev16

컨트롤러마다 권한 검사를 복붙하다 한 곳을 빠뜨린 문제

  • #Engineering Note
  • #NestJS
  • #Security

문제 발생

관리자 전용 엔드포인트를 만들면서 컨트롤러마다 이런 코드를 반복하고 있었습니다.

@Delete(":id")
async remove(@Req() req, @Param("id") id: string) {
  if (req.user?.role !== "admin") {
    throw new ForbiddenException();
  }
  return this.postService.remove(id);
}

한 곳에서 이 검사를 빠뜨렸고, 그게 배포된 뒤에야 발견됐습니다.

원인 분석

이건 핸들러의 일이 아닙니다. NestJS 공식 문서가 가드의 단일 책임을 이렇게 정의합니다 — 가드는 런타임에 존재하는 조건(권한, 역할, ACL 등)에 따라 주어진 요청을 라우트 핸들러가 처리할지 말지 결정합니다.

실행 위치도 정해져 있습니다 — 가드는 모든 미들웨어 이후, 그리고 어떤 인터셉터나 파이프보다 먼저 실행됩니다. 그래서 인가 판정이 본문 검증이나 응답 변환보다 앞섭니다.

핸들러 안에 조건문으로 흩어두면 두 가지가 무너집니다. 빠뜨린 곳을 찾을 방법이 없고, "이 엔드포인트에 어떤 권한이 필요한가"가 코드 어디에도 선언되어 있지 않습니다.

해결 방안

  1. CanActivate를 구현합니다. canActivate()는 현재 요청이 허용되는지를 boolean으로 반환하며, PromiseObservable로 비동기 반환도 가능합니다.
@Injectable()
export class RolesGuard implements CanActivate {
  constructor(private reflector: Reflector) {}

  canActivate(context: ExecutionContext): boolean {
    const required = this.reflector.get(Roles, context.getHandler());
    if (!required) return true;

    const { user } = context.switchToHttp().getRequest();
    return required.some((role) => user?.roles?.includes(role));
  }
}
  1. 역할은 데코레이터로 선언합니다. Reflector.createDecorator로 타입이 붙은 데코레이터를 만듭니다.
export const Roles = Reflector.createDecorator<string[]>();
@Roles(["admin"])
@Delete(":id")
remove(@Param("id") id: string) {
  return this.postService.remove(id);
}

이제 필요한 권한이 엔드포인트 옆에 선언되어 있습니다. 검사를 빠뜨렸는지 눈으로 확인할 수 있습니다.

  1. 범위를 골라서 붙입니다. 세 가지입니다.
@UseGuards(RolesGuard)          // 메서드 또는 컨트롤러
app.useGlobalGuards(new RolesGuard(reflector));   // 전역

전역 가드에 의존성을 주입해야 하면 모듈 provider에 APP_GUARD 토큰으로 등록합니다.

providers: [{ provide: APP_GUARD, useClass: RolesGuard }]
  1. 거부는 예외로 표현합니다. 가드가 false를 반환하면 Nest가 403 Forbidden을 자동으로 돌려줍니다. 이유를 구분해야 하면 직접 예외를 던집니다.
throw new ForbiddenException("이 작업에는 운영자 권한이 필요합니다");
  1. 가드는 인가지 소유권 검사가 아닙니다. "관리자인가"는 가드가, "이 글의 작성자인가"는 데이터 접근 지점이 판단하는 편이 안전합니다. 리소스를 조회해봐야 알 수 있는 조건을 가드에 넣으면 같은 조회가 두 번 일어납니다.

  2. 인증과 인가를 섞지 않습니다. "누구인가"를 세우는 것과 "무엇을 할 수 있는가"를 판정하는 것은 다른 가드입니다. 하나로 합치면 공개 엔드포인트를 만들 때마다 예외 처리가 늘어납니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.