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

파일 업로드 폼인데 서버에 파일 이름만 도착한 이유

  • #Engineering Note
  • #HTML
  • #Security

문제 발생

프로필 이미지 업로드 폼을 만들었는데, 서버에는 파일이 아니라 파일 이름 문자열만 도착했습니다.

<form method="post" action="/upload">
  <input type="file" name="avatar" />
  <button>업로드</button>
</form>
POST /upload
avatar=cat.png          ← 파일 내용은 어디에도 없음

브라우저 콘솔에서 input.value를 찍어보니 C:\fakepath\cat.png가 나와서 더 혼란스러웠습니다.

원인 분석

폼의 기본 인코딩은 application/x-www-form-urlencoded입니다. 이 형식은 이름=값&이름=값 형태의 텍스트라 바이너리 파일 내용을 담을 수 없습니다. 그래서 파일 이름만 값으로 전송됩니다.

파일을 실제로 보내려면 인코딩을 바꿔야 합니다. MDN의 설명대로 파일 내용을 전송하려면 method="post"enctype="multipart/form-data" 가 필요합니다 — 바이너리 데이터를 제대로 다루는 것은 이 형식뿐입니다.

C:\fakepath\는 버그가 아니라 보안 장치입니다. MDN이 명시합니다 — 값은 항상 C:\fakepath\가 붙은 파일 이름이며 실제 경로가 아닙니다. 악성 소프트웨어가 사용자의 파일 구조를 추측하지 못하게 하기 위한 것입니다.

해결 방안

  1. 폼에 두 속성을 모두 붙입니다.
<form method="post" action="/upload" enctype="multipart/form-data">
  <label for="avatar">프로필 이미지</label>
  <input type="file" id="avatar" name="avatar" accept="image/png, image/jpeg" />
  <button>업로드</button>
</form>
  1. accept는 검증이 아니라 힌트입니다. MDN이 분명히 적습니다 — accept는 선택된 파일의 타입을 검증하지 않으며, 브라우저가 사용자를 올바른 타입으로 유도하도록 돕는 힌트입니다. 대부분의 경우 사용자가 파일 선택 대화상자에서 옵션을 바꿔 이를 무시하고 아무 파일이나 고를 수 있습니다.

  2. 그래서 서버 검증이 필수입니다. 문서도 accept적절한 서버 측 검증으로 뒷받침하라고 명시합니다. 확장자가 아니라 실제 내용(매직 넘버)과 크기를 확인하고, 저장 파일명은 서버가 새로 만듭니다.

- 확장자만 믿지 않는다 (cat.png 라는 이름의 실행 파일)
- Content-Type 헤더도 클라이언트가 보낸 값이다
- 크기 제한은 앱과 리버스 프록시 양쪽에 건다
  1. 여러 개를 받으려면 multiple입니다. 서버에서도 같은 이름의 항목이 여러 개 오는 것으로 처리해야 합니다.
<input type="file" name="photos" multiple />
  1. JavaScript로 보낼 때는 FormData를 씁니다. 이 경우 Content-Type을 직접 지정하지 않습니다 — 브라우저가 boundary를 포함해 알아서 붙입니다.
const body = new FormData(form);
await fetch("/upload", { method: "POST", body });   // Content-Type 지정하지 않음
  1. 프록시 제한을 잊지 않습니다. 앱이 5MB를 허용해도 앞단 nginx의 client_max_body_size 기본값(1MB)에 걸리면 요청은 앱에 닿지도 못하고 413으로 끝납니다. 로컬에서는 프록시가 없어 재현되지 않습니다.

공식 문서

마지막 수정

좋아요북마크

댓글0

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