파일 업로드 폼인데 서버에 파일 이름만 도착한 이유
문제 발생
프로필 이미지 업로드 폼을 만들었는데, 서버에는 파일이 아니라 파일 이름 문자열만 도착했습니다.
<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\가 붙은 파일 이름이며 실제 경로가 아닙니다. 악성 소프트웨어가 사용자의 파일 구조를 추측하지 못하게 하기 위한 것입니다.
해결 방안
- 폼에 두 속성을 모두 붙입니다.
<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>-
accept는 검증이 아니라 힌트입니다. MDN이 분명히 적습니다 —accept는 선택된 파일의 타입을 검증하지 않으며, 브라우저가 사용자를 올바른 타입으로 유도하도록 돕는 힌트입니다. 대부분의 경우 사용자가 파일 선택 대화상자에서 옵션을 바꿔 이를 무시하고 아무 파일이나 고를 수 있습니다. -
그래서 서버 검증이 필수입니다. 문서도
accept를 적절한 서버 측 검증으로 뒷받침하라고 명시합니다. 확장자가 아니라 실제 내용(매직 넘버)과 크기를 확인하고, 저장 파일명은 서버가 새로 만듭니다.
- 확장자만 믿지 않는다 (cat.png 라는 이름의 실행 파일)
- Content-Type 헤더도 클라이언트가 보낸 값이다
- 크기 제한은 앱과 리버스 프록시 양쪽에 건다- 여러 개를 받으려면
multiple입니다. 서버에서도 같은 이름의 항목이 여러 개 오는 것으로 처리해야 합니다.
<input type="file" name="photos" multiple />- JavaScript로 보낼 때는
FormData를 씁니다. 이 경우Content-Type을 직접 지정하지 않습니다 — 브라우저가 boundary를 포함해 알아서 붙입니다.
const body = new FormData(form);
await fetch("/upload", { method: "POST", body }); // Content-Type 지정하지 않음- 프록시 제한을 잊지 않습니다. 앱이 5MB를 허용해도 앞단 nginx의
client_max_body_size기본값(1MB)에 걸리면 요청은 앱에 닿지도 못하고 413으로 끝납니다. 로컬에서는 프록시가 없어 재현되지 않습니다.
댓글0
댓글을 남기려면 로그인이 필요해요. 로그인
아직 댓글이 없어요. 첫 의견을 편하게 남겨 보세요.