📁 File0. 파일의 기초03 — MIME & Magic Numbers

03 — MIME & Magic Numbers

한 줄 답: 진짜 파일 타입은 *확장자가 아니라 첫 몇 바이트(매직넘버)*에 있다. MIME 타입은 그 결과를 프로토콜이 이해할 수 있는 표준 라벨로 표현한 것이다.


Why — 왜 매직과 MIME이 모두 필요한가

hello.jpg라는 파일이 있다. 이게 정말 JPEG인지 어떻게 알 수 있나?

방법신뢰도비용
확장자 보기 (.jpg)낮음 — 누구나 바꿀 수 있음O(1), 즉시
파일 첫 바이트 보기 (FF D8 FF)높음 — 위조하려면 파일 자체를 만들어야 함O(상수) — 16~512바이트만 읽으면 끝
파일 전체 파싱최고O(파일크기)

→ 1980년대부터 UNIX의 file(1) 명령은 첫 몇 바이트의 패턴으로 파일을 식별해왔다. 이 패턴을 매직넘버(magic number) 또는 **파일 시그니처(file signature)**라고 부른다.

그러나 매직만으로는 부족하다. 메일·웹·API에서 파일을 주고받을 때, 그 결과를 표현할 표준 라벨이 필요했다 → 이게 MIME 타입이다.

[원시 바이트]  →  [매직 검사]  →  [MIME 타입 라벨]
FF D8 FF E0 ...      JPEG?            image/jpeg

매직 = 증거, MIME = 선언. 둘은 다른 레이어에 산다.


How — 어떻게 동작하나

1) MIME 타입의 구조

MIME = Multipurpose Internet Mail Extensions. 1992년 RFC 1341, 현재 RFC 6838에서 표준화.

   image / png ; charset=utf-8
   ─────   ───   ──────────────
   type / subtype  parameters (optional)
Top-level type의미대표 subtype
text사람이 읽을 수 있는 텍스트text/plain, text/html, text/css
image정지 영상image/png, image/jpeg, image/webp, image/avif
audio소리audio/mpeg, audio/aac, audio/ogg
video동영상video/mp4, video/webm, video/x-matroska
application그 외 모든 바이너리application/pdf, application/zip, application/json, application/octet-stream
multipart여러 부분의 묶음multipart/form-data, multipart/mixed
model3D 모델model/gltf-binary
font폰트font/woff2, font/ttf

비표준은 x- 또는 vnd. 접두사:

  • application/vnd.ms-excel — 벤더 등록
  • application/x-tar — 비공식

구조화된 syntax 접미사:

  • application/ld+json (+json) — JSON-LD
  • image/svg+xml (+xml) — SVG는 XML로 파싱 가능
  • application/vnd.api+json (+json) — JSON:API

2) 매직넘버 — 파일이 자기 정체를 신고하는 첫 바이트들

포맷매직 (hex)위치ASCII 표현
PNG89 50 4E 47 0D 0A 1A 0A0~7\x89PNG\r\n\x1a\n
JPEGFF D8 FF0~2
GIF47 49 46 38 (37|39) 610~5GIF87a / GIF89a
WebP52 49 46 46 ?? ?? ?? ?? 57 45 42 500~11RIFF....WEBP
BMP42 4D0~1BM
PDF25 50 44 46 2D0~4%PDF-
ZIP / docx / xlsx50 4B 03 040~3PK.. (ZIP은 PKZIP에서 유래)
RAR52 61 72 21 1A 07 000~6Rar!..
7z37 7A BC AF 27 1C0~57z..'.
MP3 (ID3v2)49 44 330~2ID3
MP3 (frame)FF FB 또는 FF FA0~1
MP4 / MOV?? ?? ?? ?? 66 74 79 704~7....ftyp
WAV52 49 46 46 ?? ?? ?? ?? 57 41 56 450~11RIFF....WAVE
FLAC66 4C 61 430~3fLaC
OGG4F 67 67 530~3OggS
ELF (Linux exe)7F 45 4C 460~3\x7fELF
Mach-O (macOS exe)CF FA ED FE0~3(LE)
Windows PE4D 5A0~1MZ
GZIP1F 8B0~1
XML3C 3F 78 6D 6C0~4<?xml
HTML(<!DOCTYPE / <html 등 패턴)가변

어떤 매직은 파일 끝에 있기도 하다 (ZIP의 End-of-Central-Directory). 그래서 진짜 파서는 머리 + 꼬리를 모두 검사한다.

3) file(1) 명령과 libmagic

UNIX의 file 명령은 /usr/share/file/magic 같은 매직 데이터베이스를 보고 판정한다.

$ file photo.jpg
photo.jpg: JPEG image data, JFIF standard 1.01, aspect ratio, density 1x1, ...
 
$ file photo.jpg --mime-type
photo.jpg: image/jpeg
 
$ file --mime hello.txt
hello.txt: text/plain; charset=utf-8

라이브러리 형태인 libmagic은 거의 모든 언어에서 바인딩이 있다.

import magic
mime = magic.from_file("photo.jpg", mime=True)
# 'image/jpeg'
 
mime = magic.from_buffer(open("photo.jpg","rb").read(2048), mime=True)
import { fileTypeFromBuffer } from 'file-type';
const result = await fileTypeFromBuffer(buffer);
// { ext: 'jpg', mime: 'image/jpeg' }

4) Content-Type이 결정되는 흐름 (HTTP)

실무 결론: 절대로 클라이언트가 보낸 Content-Type만 믿지 말고 서버에서 매직 검사를 한 번 더 한다.


What — 구체 사양·수치

MIME 타입 표준 등록처

  • IANA Media Types Registryhttps://www.iana.org/assignments/media-types
  • 새 타입은 RFC 6838 절차에 따라 등록 (Standards Tree / Vendor Tree / Personal Tree)
  • 예: image/avif는 2019년 12월 등록, image/jxl(JPEG XL)은 2022년 등록

자주 쓰이는 MIME 타입 표

카테고리MIME확장자
텍스트text/plain.txt
text/html.html, .htm
text/css.css
text/csv.csv
text/markdown.md
이미지image/jpeg.jpg, .jpeg
image/png.png
image/webp.webp
image/avif.avif
image/gif.gif
image/svg+xml.svg
image/heic.heic (iPhone)
오디오audio/mpeg.mp3
audio/aac.aac
audio/wav.wav
audio/ogg.ogg, .opus
audio/flac.flac
비디오video/mp4.mp4, .m4v
video/webm.webm
video/quicktime.mov
video/x-matroska.mkv
문서application/pdf.pdf
application/vnd.openxmlformats-officedocument.wordprocessingml.document.docx
application/vnd.ms-excel.xls
application/json.json
application/xml.xml
압축application/zip.zip
application/gzip.gz
application/x-tar.tar
application/x-7z-compressed.7z
폰트font/woff2.woff2
스트리밍application/vnd.apple.mpegurl.m3u8 (HLS manifest)
application/dash+xml.mpd (DASH manifest)
video/mp2t.ts (HLS segment)
알 수 없음application/octet-stream(모든 바이너리의 fallback)

매직넘버 검사 코드 예 (Node.js)

import { open } from 'node:fs/promises';
 
const PNG_SIG = Buffer.from([0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]);
 
async function isPng(path) {
  const fd = await open(path, 'r');
  try {
    const buf = Buffer.alloc(8);
    await fd.read(buf, 0, 8, 0);
    return buf.equals(PNG_SIG);
  } finally {
    await fd.close();
  }
}

매직과 MIME이 1:1이 아닌 케이스

매직매핑되는 MIME이유
PK\x03\x04application/zip 또는 .docx, .xlsx, .pptx, .jar, .apk, .epubOOXML/JAR/APK는 전부 ZIP. 내부 구조를 더 봐야 구분
....ftyp (MP4)video/mp4, audio/mp4, video/quicktime, image/heicftyp 박스 안의 brand 코드(isom, mp42, qt , heic)로 구분
RIFFimage/webp, audio/wav, video/aviRIFF 다음 4바이트(WEBP/WAVE/AVI )로 구분
<?xmlapplication/xml, image/svg+xml, application/atom+xml루트 element를 봐야 함

함의: 매직은 시작점이지 결론이 아니다. 정확한 MIME 결정은 컨테이너 내부까지 파싱해야 가능한 경우가 많다.

브라우저의 Content Sniffing

브라우저는 서버가 보낸 Content-Type을 항상 신뢰하지 않는다 — 오래된 IIS·Apache가 잘못된 헤더를 자주 보냈기 때문에, 표준(MIME Sniffing, WHATWG)이 바이트 검사로 재추론하는 절차를 정의한다.

이게 보안 사고의 단골 원인:

  • 공격자가 evil.htmlimage.png라고 업로드 → 서버가 image/png로 회신 → 브라우저가 내용을 보고 HTML로 추론 → XSS 실행

방어: X-Content-Type-Options: nosniff 헤더를 항상 보내라. 그러면 브라우저가 sniffing을 비활성화하고 서버가 선언한 Content-Type만 믿는다.


What-if — 잘못 쓰면

1) 확장자만 믿고 처리하면

# 위험한 코드
if filename.endswith('.jpg'):
    process_as_image(filename)
  • 공격자가 .jpg 확장자로 PHP 스크립트를 업로드해 서버에서 실행시키는 사고가 2010년대 PHP 호스팅에서 흔했다 (Local File Inclusion).
  • 해결: 항상 매직 검사 + 처리 전 안전한 확장자로 재명명

2) 클라이언트가 보낸 Content-Type만 믿으면

POST /upload
Content-Type: image/png   ← 공격자가 마음대로 설정
[실제로는 EICAR 바이러스 시그니처]

→ 서버가 그대로 저장 + S3에 Content-Type: image/png로 올림 → 사용자가 다운로드할 때 브라우저가 image로 처리해 별일 없을 수도 있고, sniffing이 켜져 있으면 exe로 실행될 수도 있다.

업로드 시점에 서버 측 매직 검증 + S3에 올릴 때 검증된 MIME으로 명시.

3) image/svg+xml진짜 위험하다

SVG는 텍스트(XML) 기반이라 <script> 태그를 포함할 수 있다. 사용자가 SVG 아바타를 올렸는데, 그 안에 <script>fetch('/admin', {credentials:'include'})...</script>가 들어있다면?

→ SVG는:

  • 별도 도메인에서 호스팅 (cookie 격리)
  • 또는 <img src=...>로만 사용 (이때 스크립트 비활성화됨)
  • 또는 Content-Disposition: attachment로 다운로드만 허용
  • 절대로 <iframe>이나 <object>로 임베드하지 말 것

4) ZIP-bomb과 폴리글랏 파일

  • ZIP bomb: 42KB ZIP이 압축 풀면 4.5PB가 되는 케이스 (42.zip). 매직만 보고 자동 압축 풀면 디스크가 가득 참.
  • Polyglot: 한 파일이 여러 매직을 동시에 만족. PDF + JAR + ZIP이 모두 유효한 동일 파일을 만들 수 있다 (CVE-2010-XXXX 시리즈).

방어: 압축 풀 때 해제 후 크기 한도, 파일 개수 한도, 깊이 한도를 강제.

5) macOS resource fork와 __MACOSX/

macOS의 Finder에서 ZIP을 만들면 .DS_Store, __MACOSX/._filename 같은 부산물이 들어간다. → Linux에서 압축 해제 시 유령 파일들이 추가됨. CI 환경에서 unzip -d로 풀 때 골치.

6) application/octet-stream fallback의 함정

서버가 MIME을 모르면 application/octet-stream을 보낸다. 브라우저는 이걸 무조건 다운로드로 처리한다. → *.webp 같은 신생 포맷이 nginx의 기본 mime.types에 없으면 사용자에게 다운로드 창이 뜬다. → 해결: mime.types를 최신화하거나 text/plain 강제하지 말고 정확한 MIME 명시.


Insight — 흥미로운 이야기

”MIME은 원래 이메일용이었다”

이름 그대로 Multipurpose Internet Mail Extensions. 1992년 SMTP에 ASCII 외 데이터(이미지, 첨부)를 실으려고 만들었다. RFC 1341에서 type/subtype 형식을 정한 그 약속을 **1996년 HTTP/1.0 (RFC 1945)**이 가져다 썼다. 그래서 오늘날 웹 전체가 이메일 표준을 빌려 쓰고 있다 — Content-Type, Content-Disposition, Content-Transfer-Encoding은 모두 이메일 출신.

”PDF의 매직 %PDF-주석이다”

%PDF-1.7%는 PDF 파일 안에서 주석을 시작하는 문자다. 즉 PDF 파서는 첫 줄을 읽으면서 무시한다. 이건 의도적이다 — 매직넘버 자체가 PDF 문법의 일부라서, 다른 도구가 시작 부분을 잘못 건드려도 PDF는 여전히 유효하다.

”PNG 매직의 천재성”

89 50 4E 47 0D 0A 1A 0A — 8바이트 안에 다음을 모두 검출한다:

  • 89 (high bit set) → 7비트만 보존하는 시스템에서 깨졌는지 검출
  • 50 4E 47 = PNG → 사람이 읽을 수 있는 라벨
  • 0D 0A → CRLF 줄바꿈 변환되었는지 검출 (Windows ↔ Unix)
  • 1A → DOS의 EOF 문자 → DOS type 명령이 여기서 멈춤
  • 0A → LF → 위 CRLF가 LF로 변환되었는지 다시 검출

→ 1995년 PNG 설계자들이 FTP의 ASCII 모드 전송이 파일을 깨뜨리던 시대의 모든 함정을 한 헤더에 인코딩했다. 8바이트짜리 자기진단 시스템. 이런 디테일이 표준의 품질을 만든다.

”Apple이 HEIC를 밀어붙인 이유”

2017년 iPhone 7부터 사진 기본 포맷이 JPEG → HEIC로 바뀌었다.

  • HEIC = HEVC(H.265)로 압축한 이미지 컨테이너 (.heif/.heic)
  • 같은 화질에서 JPEG보다 ~50% 작음
  • 매직: MP4와 같은 ftyp 박스 (brand가 heic/mif1)

→ MIME(image/heic)이 늦게 등록(2017)되어 한동안 안드로이드가 못 열었다. 매직과 MIME의 표준화 속도가 기술 보급보다 항상 늦는다.


요약 + 다이어그램

매직넘버 = 파일이 자기 정체를 첫 바이트로 신고하는 것. MIME = 그 결과를 인터넷 프로토콜이 이해할 수 있게 라벨링한 것. 실무 규칙 — (1) 클라이언트 Content-Type 절대 신뢰 X, (2) 서버에서 항상 매직 검사, (3) X-Content-Type-Options: nosniff 항상 켜기, (4) SVG/ZIP 폴리글랏 주의.

다음 문서: 04-extensions-and-identification.md — 확장자·MIME·매직이 어긋날 때 무슨 일이 일어나는가?