보안 관점에서 이해하는 Pillow의 open(), verify(), load()
이미지 업로드는 “그림 파일을 받는다”가 아니라, 외부 입력을 디코더(파서)에 통과시키는 일입니다. 그래서 Pillow의 세 메서드는 기능 설명보다도, 언제 호출하느냐(=공격 표면을 언제 열어주느냐)가 핵심입니다.
open()은 “픽셀을 올리는 함수”가 아니다
Image.open()은 lazy 동작입니다. 즉, 파일을 “열고 식별”만 하고, 픽셀 데이터는 아직 읽지 않을 수 있습니다. 그리고 파일 핸들이 열린 채로 남을 수 있습니다.
보안/운영에서 open()을 잘 쓰는 방식은 단순합니다.
open()으로 포맷 식별, 폭/높이 같은 가벼운 정보를 먼저 얻고- 정책으로 차단: 허용 포맷, 최대 해상도/픽셀 수, 업로드 용량 제한
- 그 다음 단계(검증/디코딩)를 진행
즉, open()은 “디코딩 전 단계에서 판단할 정보를 뽑는 도구”로 쓰는 게 안전합니다.
verify()는 무엇을 보장하고, 무엇을 보장하지 않는가
Pillow의 verify()는 “파일이 깨졌는지”를 확인하려고 시도하지만, 실제 이미지 데이터를 디코딩하지 않고 검사합니다. 문제가 있으면 예외를 던지고, verify() 후에 이미지를 계속 쓰려면 파일을 다시 열어야 합니다.
여기서 보안 관점의 결론은 두 가지입니다.
- 장점: 디코딩(=무거운 작업)을 피하면서 “망가진 파일”을 빠르게 걸러낼 수 있음
- 한계:
verify()통과는 “안전”이 아니라 “당장 크게 깨져 보이지 않음”에 가깝습니다. 디코딩을 끝까지 수행하지 않기 때문에,load()시점에야 드러나는 문제도 존재할 수 있습니다.
load()는 검증 단계에서 함부로 호출하면 위험하다
load()는 실제로 디코딩(압축 해제 포함)을 수행해 픽셀을 메모리에 올리는 단계입니다.
이 지점이 곧바로 DoS(자원 고갈) 공격 표면이 됩니다. 겉보기 파일 크기가 작아도, 디코딩 결과가 매우 커질 수 있기 때문입니다.
Pillow는 “디컴프레션 봄” 위험을 경고/예외로 다루며, 기본 임계치(예: 128Mpx 수준) 같은 보호 장치를 두고 있습니다.
Django도 같은 이유로, 이미지 업로드 검증에서 load() 대신 verify()를 씁니다. 소스에 “load()는 전체 이미지를 메모리에 올려 DoS 벡터가 된다”는 코멘트가 있고, 실제로 Image.open() 후 verify()를 호출합니다.
Django/DRF 사용 시: ImageField에서 verify()를 또 하면 중복일 수 있다
Django의 폼 ImageField 검증이 내부에서 Image.open() + verify()를 수행한다는 건 앞에서 본 그대로입니다.
DRF의 serializers.ImageField도 이미지 검증을 “Django 구현에 위임한다”는 주석과 함께 Django 쪽 검증을 호출하는 흐름을 갖습니다.
그래서 DRF에서 serializers.ImageField를 이미 쓰고 있다면:
- 단순히 “손상 여부 확인”을 위해
validate()에서verify()를 또 호출하는 건 대개 중복 작업이 됩니다. - 비즈니스 검증/추가 보안 검증을 강하게 커스터마이즈하려면,
ImageField대신FileField로 받고(검증 파이프라인을 직접 설계) 비용과 책임을 명확히 가져가는 선택이 더 깔끔할 수 있습니다.
사용자가 업로드한 파일에 대해 “안전”을 확보하려면

가장 현실적인 답은 이겁니다.
“업로드 원본을 그대로 쓰지 말고, 서버가 디코딩해서 새 파일로 다시 저장한 결과물만 사용한다.”
open()으로 폭/높이/포맷 등 저비용 정보를 읽고 정책으로 1차 차단verify()로 명백히 깨진 파일을 제거- 그 다음(통과한 것만) 제한된 환경에서 디코딩 후 RGB/RGBA 같은 표준 픽셀로 정규화
- 서버가 선택한 포맷으로 재인코딩하여 새 파일 생성
- 서비스는 서버가 재생성한 파일만 저장/서빙
이 전략의 장점은 “서버가 최종 출력의 형태를 통제한다”는 점입니다. 원본에 있던 불필요한 메타데이터나 이상한 구조를 상당 부분 제거할 수 있습니다.
단, 재인코딩은 결국 load()에 준하는 디코딩을 포함합니다.
따라서 픽셀 수/메모리 제한(디컴프레션 봄 방어) 같은 가드레일을 먼저 세우고, 가능하면 워커/격리 프로세스에서 실행하는 것이 안전합니다.
정리
open(): “식별 + 저비용 정보 확인” 단계(픽셀은 아직 아닐 수 있음)verify(): “손상 여부 1차 필터”(디코딩 없이 검사, 이후 사용하려면 재오픈)load(): “디코딩/메모리 사용이 시작되는 지점”(검증 단계에서 남발 금지)- 업로드 안전의 실무 답: 서버가 재인코딩한 결과물만 신뢰(단, 제한/격리 필수)
관련글 보기 :