콘텐츠로 건너뛰기

웹 양식 파일 검증 구현 체크리스트

카테고리: 보안·구현
이 기사는 현재 일본어로만 제공됩니다. 번역본은 순차적으로 공개될 예정입니다.

파일 업로드 기능의 구현에는 보안상 주의할 점이 많고 놓치기 쉬운 함정도 많습니다. 본 문서에서는 프로덕션 환경에서 안전하게 작동하는 파일 업로드를 구현하기 위한 체크리스트를 설명합니다.

파일 유효성 검증 권장 순서 안전한 순서: 가벼운 검증에서 무거운 검증으로 1. 파일 크기 bytes <= max NG → 즉시 거부 2. 확장자 .jpg / .png ... NG → 즉시 거부 3. MIME 타입 finfo / mime_content_type NG → 즉시 거부 4. 매직 바이트 FF D8 FF (JPEG) ... NG → 즉시 거부 5. 내용 스캔 ClamAV / VirusTotal NG → 즉시 거부
그림 1: 가벼운 검증부터 실행하여 조기 거부

체크리스트 개요

이 체크리스트는 <strong>백엔드(서버 측) 검증</strong>을 중심으로 정리되어 있습니다. 프론트엔드 검증은 보조적인 UX 개선으로 구현되지만 보안 보장이 되지 않습니다.

1. 파일 크기 검증

  • <input type="checkbox" disabled> 업로드 상한을 바이트 단위로 정의하고 있습니다(MB와 MiB의 혼동 없음).
  • <input type="checkbox" disabled> PHP의 경우, <code>upload_max_filesize</code>와 <code>post_max_size</code> 둘 다 설정하고 있습니다.
  • <input type="checkbox" disabled> Nginx의 경우, <code>client_max_body_size</code>에 multipart 오버헤드분을 추가하고 있습니다.
  • <input type="checkbox" disabled> <code>$_FILES['file']['error']</code>가 UPLOAD_ERR_INI_SIZE / UPLOAD_ERR_FORM_SIZE인 경우의 에러 처리가 있습니다.
  • <input type="checkbox" disabled> 최소 파일 크기 검사가 있습니다(0바이트 파일 제외).
 $maxBytes) {
        throw new \RuntimeException(sprintf(
            'ファイルサイズ(%s)が上限(%s)を超えています',
            number_format($file['size']),
            number_format($maxBytes)
        ));
    }
}

2. 파일 형식 검증(MIME 타입)

  • <input type="checkbox" disabled> 클라이언트에서 보낸 <code>Content-Type</code>(<code>$_FILES['file']['type']</code>)을 신뢰하지 않습니다.
  • <input type="checkbox" disabled> <code>finfo</code> / <code>mime_content_type()</code>으로 서버 측 MIME 타입 검증을 수행하고 있습니다.
  • <input type="checkbox" disabled> 허용하는 MIME 타입의 화이트리스트를 정의하고 있습니다.
file($file['tmp_name']);

if (!in_array($mimeType, $allowed, true)) {
    throw new \RuntimeException('許可されていないファイル形式です: ' . $mimeType);
}

3. 파일 확장자 검증

  • <input type="checkbox" disabled> 확장자의 화이트리스트를 정의하고 있습니다(블랙리스트가 아닌 화이트리스트).
  • <input type="checkbox" disabled> 이중 확장자(<code>shell.php.jpg</code>)를 감지하고 거부하고 있습니다.
  • <input type="checkbox" disabled> 대소문자를 정규화하여 검증하고 있습니다(<code>.JPG</code>와 <code>.jpg</code>를 동일시).
 2) {
    throw new \RuntimeException('不正なファイル名です');
}

$ext = strtolower(pathinfo($originalName, PATHINFO_EXTENSION));
if (!in_array($ext, $allowed, true)) {
    throw new \RuntimeException('許可されていない拡張子です: ' . $ext);
}

4. 매직 바이트(파일 서명) 검증

  • <input type="checkbox" disabled> 중요한 파일(실행 파일 제외 등)에서 매직 바이트를 검증하고 있습니다.

5. 저장 위치 및 파일 이름의 안전한 처리

  • <input type="checkbox" disabled> 저장 디렉토리는 웹 루트 외부입니다(또는 XSendFile/X-Accel-Redirect로 제어).
  • <input type="checkbox" disabled> 저장 파일명은 UUID 등으로 무작위 생성하고 원래 파일명을 사용하지 않습니다.
  • <input type="checkbox" disabled> 경로 순회(<code>../../../etc/passwd</code>)를 검증으로 배제하고 있습니다.
  • <input type="checkbox" disabled> 저장 디렉토리에 PHP 실행 권한이 없습니다(<code>.htaccess</code> 또는 Nginx 설정으로 PHP 처리 비활성화).

6. 에러 핸들링 및 레스폰스

  • <input type="checkbox" disabled> 업로드 성공 시 적절한 HTTP 상태(200/201)를 반환하고 있습니다.
  • <input type="checkbox" disabled> 크기 초과 시 <strong>413 Payload Too Large</strong>를 반환하고 있습니다.
  • <input type="checkbox" disabled> 잘못된 파일 형식 시 <strong>422 Unprocessable Entity</strong>를 반환하고 있습니다.
  • <input type="checkbox" disabled> 에러 메시지에 서버의 내부 정보(경로, 버전 등)가 포함되어 있지 않습니다.

7. 테스트 케이스

구현 후 다음 테스트 케이스를 실행하여 동작을 확인하세요. DevLab의 테스트 파일을 활용할 수 있습니다.

테스트 케이스예상 결과사용할 파일
정확히 한계 크기의 파일성공<a href="/ja/files/threshold/">임계값 파일</a>
한계를 1바이트 초과하는 파일413 에러<a href="/ja/files/threshold/">임계값 파일</a>
0바이트의 빈 파일검증 오류수동 생성
확장자를 위장한 파일 (PHP→<code>.jpg</code>)MIME 오류<a href="/ja/files/broken/">손상된 파일</a>
헤더 손상 파일검증 오류<a href="/ja/files/broken/">손상된 파일</a>

요약

안전한 파일 업로드 구현에는 여러 계층에서의 검증이 필수적입니다. 특히 다음 3가지는 반드시 구현해야 합니다.

  1. <strong>서버 측 MIME 타입 검증</strong>(<code>finfo</code> 사용) — 클라이언트 신고를 신뢰하지 않기
  2. <strong>임의의 파일명으로 저장</strong> — 원본 파일명 사용 금지
  3. <strong>업로드 디렉토리에서 PHP 실행 비활성화</strong>——업로드 디렉토리에서 스크립트가 실행되지 않도록 방지

이 기사에서 사용할 수 있는 테스트 파일

  • → <a href="/ja/files/broken/" class="text-primary-600 dark:text-primary-400 hover:underline">손상된 파일 목록(검증 에러 테스트용)</a>
  • → <a href="/ja/files/threshold/" class="text-primary-600 dark:text-primary-400 hover:underline">임계값 테스트 파일 목록 (9.9MB / 10MB / 10.1MB)</a>
  • → <a href="/ja/files/images/" class="text-primary-600 dark:text-primary-400 hover:underline">이미지 테스트 파일 목록 (PNG / JPG / WebP / GIF)</a>

관련 기사

  • → <a href="/ja/blog/how-to-test-upload-limit/" class="text-primary-600 dark:text-primary-400 hover:underline">파일 업로드 제한을 올바르게 테스트하는 방법</a>
  • → <a href="/ja/reference/magic-bytes/" class="text-primary-600 dark:text-primary-400 hover:underline">매직 바이트 (파일 시그니처) 참고</a>