← 전체 글로 돌아가기

Docker

이미지 업로드 API에 파일 크기 제한을 두기 전에 정한 다섯 가지

용량 숫자만 검사해서는 부족했던 이미지 업로드 API의 검증 항목을 체크리스트로 정리했습니다.

10MB 제한만으로는 안심할 수 없었다

프로필 이미지 업로드에 10MB 이하 조건을 넣고 끝냈는데, 테스트 중 확장자가 .jpg인 텍스트 파일도 통과했다. 브라우저가 보낸 Content-Type도 신뢰할 수 없었다. 업로드는 저장소 비용 문제이면서 동시에 입력 검증 문제다.

아래 기준은 사용자가 올린 파일을 웹에서 다시 제공하는 작은 서비스에 적용했다.

업로드 경로 체크리스트

  • 요청 본문의 최대 크기를 프록시와 애플리케이션 양쪽에서 제한한다.
  • 파일명 확장자가 아니라 파일 내용의 MIME 타입을 검사한다.
  • 허용한 형식만 디코딩해 실제 이미지인지 확인한다.
  • 원본 파일명으로 저장하지 않고 서버가 만든 식별자를 쓴다.
  • 공개 URL과 업로드 권한을 분리하고, 필요하면 바이러스 검사 대기 상태를 둔다.

Nginx의 제한은 큰 요청이 앱까지 도달하는 일을 줄인다.

server {
  client_max_body_size 10m;
}

다만 이것만으로 API 규칙이 되지는 않는다. 애플리케이션도 파일이 없는 경우, 개수 초과, 허용하지 않은 타입을 각각 400 또는 413으로 처리해야 클라이언트가 원인을 알 수 있다.

타입은 선언값과 실제 내용을 같이 본다

Node 환경에서는 업로드 미들웨어가 제공하는 mimetype을 첫 필터로 쓸 수 있지만 최종 판정으로 삼지 않았다. 이미지 라이브러리로 메타데이터를 읽어 디코딩 실패를 거르고, 허용 목록을 좁혔다.

const allowed = new Set(['image/jpeg', 'image/png', 'image/webp'])

if (!file || !allowed.has(file.mimetype)) {
  return Response.json({ error: 'unsupported image type' }, { status: 400 })
}

const metadata = await sharp(file.buffer).metadata()
if (!metadata.width || !metadata.height) {
  return Response.json({ error: 'invalid image' }, { status: 400 })
}

실제 구현에서는 픽셀 수 제한도 추가했다. 파일 크기는 작아도 지나치게 큰 해상도는 메모리를 많이 쓸 수 있기 때문이다. SVG는 스크립트와 외부 참조 처리 규칙이 달라 처음에는 허용 목록에서 뺐다.

배포 뒤에 확인할 일

  1. 0바이트 파일, 제한보다 큰 파일, 위장 확장자 파일을 각각 올린다.
  2. 업로드 실패 메시지가 내부 저장소 경로를 노출하지 않는지 본다.
  3. 저장된 파일의 URL이 원본 파일명을 포함하지 않는지 확인한다.
  4. 성공한 파일도 이미지 변환·삭제 정책에 맞게 정리되는지 본다.

제한 값 하나를 고르는 것보다, 서버가 받은 바이트를 어디까지 믿을지 정하는 일이 업로드 API의 핵심이었다.