PDF라는데 PNG인 첨부파일: Python으로 확장자와 앞 8바이트 대조하기
보고서.pdf라는 이름을 보고 열었는데 이미지 파일이었다면, 이름과 파일 앞부분을 따로 확인해 보세요. 이 실습에서는 같은 69바이트짜리 가상 PNG를 01_known.png와 02_named_pdf.pdf라는 두 이름으로 준비했습니다. Python은 두 파일에서 같은 PNG 서명을 관측하고, 두 번째 파일만 이름과 앞부분이 다르다고 기록합니다.
여기서 확인하는 것은 확장자와 알려진 앞부분 표식의 대응입니다. 파일 전체 형식, 본문 내용, 정상 동작, 악성 여부를 판정하지 않습니다. 검사 결과에 따라 파일을 자동으로 열거나 이름을 고치는 기능도 없습니다.
확장자와 앞부분은 각각 무엇을 알려 줄까
확장자는 파일 이름 끝의 .pdf 같은 표기입니다. 이름은 바꿀 수 있으므로 확장자만으로 실제 바이트를 알 수 없습니다. 파일 서명은 형식 규격에서 정한 앞부분의 바이트 패턴입니다. 예를 들어 PNG의 시작 8바이트는 89 50 4e 47 0d 0a 1a 0a입니다. 그 뒤의 이미지 구조는 별도 요건이 있으므로 서명만 맞는 8바이트 파일도 정상 이미지로 인정할 수 없습니다. W3C PNG 규격 5.2
PDF에는 %PDF-로 시작하는 헤더 규칙이 있습니다. RFC 8118 8절의 PDF 미디어 타입 설명은 이 접두와 뒤따르는 버전 번호가 US-ASCII로 표기됨을 설명합니다. 이 RFC는 정보성 MIME 등록 문서입니다. 이 예제는 처음 다섯 문자의 접두만 관측합니다. 그 뒤의 버전 표기나 PDF 본문 구조를 검사하지 않으므로 %PDF-XYZ 같은 임의의 8바이트도 PDF_PREFIX로 기록됩니다. 또한 PDF 1.4부터 Catalog의 Version이 헤더 버전을 대체할 수 있어 헤더만으로 실제 PDF 버전을 확정할 수 없습니다. 미국 의회도서관의 PDF 1.7 형식 설명
ZIP은 이 실습에서 세 가지 기록 표식을 관측합니다. 50 4b 03 04는 로컬 헤더, 50 4b 05 06은 중앙 디렉터리 끝 기록과 연결되며, 50 4b 07 08은 분할 표식 또는 데이터 기술자 문맥에서 쓰입니다. 기록의 전체 구조나 위치 관계는 검사하지 않습니다. XLSX와 DOCX도 ZIP 컨테이너를 사용하므로 이 표식만 보고 Excel 파일인지 압축 파일인지 구별할 수 없습니다. PKWARE ZIP 규격 6.3.10
세 가지 가상 입력부터 확인하기
다운로드한 실습 ZIP을 별도 연습 폴더에 풀어 주세요. 코드가 들어 있는 최상위 폴더에서 명령을 실행합니다. samples는 그 폴더 안의 하위 폴더입니다. 회사 자료나 출처를 모르는 첨부파일로 먼저 시험할 필요는 없습니다.
- samples/01_known.png: 69바이트 가상 PNG. 앞 8바이트는 PNG 서명입니다.
- samples/02_named_pdf.pdf: 01_known.png와 완전히 같은 69바이트를 담았습니다. 이름만 .pdf입니다.
- samples/03_unknown.bin: 9바이트 임의 자료입니다. .bin은 지원하지 않아 내용도 읽지 않습니다.
지원 확장자는 .png, .pdf, .zip 세 가지입니다. 대문자 .PDF도 소문자로 비교합니다. report.pdf.exe의 마지막 확장자는 .exe이므로 지원안함입니다. 확장자가 없거나 .xlsx와 .docx인 파일도 지원안함으로 남깁니다. 지원하지 않는 파일의 존재 여부와 내용은 검사하지 않습니다. Python pathlib의 suffix 설명
Python으로 보고서 만들기
Python 3.12가 설치된 환경에서 먼저 버전을 확인합니다. 아래 명령은 python3 실행 파일을 사용하는 표기입니다. Windows Python Launcher에 3.12가 설치돼 있다면 python3을 py -3.12로 바꿔 사용할 수 있습니다. 이 글의 실제 실행 검수는 Linux CPython 3.12.14에서 했고, Windows와 Office 앱은 실행하지 않았습니다. 외부 패키지 설치는 필요 없습니다.
python3 --version
python3 header_audit.py --output report_01.json samples/01_known.png samples/02_named_pdf.pdf samples/03_unknown.bin
코드는 선택한 경로만 처리하고 폴더를 재귀 탐색하지 않습니다. report_01.json은 새 파일로 생성합니다. 입력과 같은 경로를 출력으로 지정하거나, 이미 있는 출력 파일을 지정하면 멈춥니다.
기본 예제의 기대 결과는 다음과 같습니다.
- 선택 경로 3개, 읽어서 반환한 헤더 내용 합계 16바이트
- HEADER_AGREES 1개: 01_known.png의 이름과 PNG 서명이 대응합니다.
- HEADER_DIFFERS 1개: 02_named_pdf.pdf의 이름은 PDF인데 앞부분에서 PNG 서명이 관측됩니다.
- UNSUPPORTED_EXTENSION 1개: 03_unknown.bin은 내용 읽기 0바이트입니다.
- INCONCLUSIVE, SKIPPED, READ_ERROR는 각각 0개입니다.
터미널의 REPORT_WRITTEN과 종료 코드 0은 JSON을 저장했다는 뜻입니다. 파일이 모두 정상이라는 뜻으로 읽으면 안 됩니다. report_01.json을 텍스트 편집기로 열고 rows에서 name, suffix, head_hex, observed_header, status를 함께 확인하세요. 02_named_pdf.pdf의 head_hex는 89 50 4e 47 0d 0a 1a 0a, observed_header는 PNG_SIGNATURE, status는 HEADER_DIFFERS여야 합니다.

결과에 따라 무엇을 해야 할까
HEADER_DIFFERS라면 파일 이름과 관측한 앞부분이 다릅니다. 실제 업무에서는 전달자에게 파일 형식과 원본을 확인하고 다시 받는 편이 좋습니다. 이 도구는 PNG_SIGNATURE가 보인다는 이유만으로 .pdf를 .png로 바꾸지 않습니다. 여러 형식이 섞이거나 손상된 파일의 전체 구조는 여기서 확인하지 않았기 때문입니다.
HEADER_AGREES라면 이름과 지원하는 표식이 대응했습니다. 파일 본문이 완전하거나 열 수 있다는 보장은 없습니다. 특히 .zip에서는 ZIP 기록 표식과 이름이 대응한다는 뜻만 남깁니다. 내부 목록, CRC, 암호화, 압축 해제 크기를 확인한 결과가 아닙니다.
INCONCLUSIVE라면 8바이트를 채우지 못했거나 지원 표식을 관측하지 못했습니다. 파일이 잘못됐다고 결론 내리지 말고 크기, 전달 방식과 원본을 따로 확인하세요. 이 코드는 오프셋 0에서만 읽습니다. %PDF-가 처음 8바이트 이후에 있으면 PDF가 아니라고 단정하지 않고 판단보류로 남깁니다.
UNSUPPORTED_EXTENSION이라면 지원하지 않는 이름입니다. SKIPPED는 링크나 폴더 같은 지원하지 않는 파일 대상이나 .. 구성요소가 있는 지원 입력 경로를, READ_ERROR는 읽기 실패나 읽는 동안의 상태 변화를 뜻합니다. read_note에 LINK_PATH, NOT_REGULAR, PARENT_COMPONENT, READ_ERROR, CHANGED_DURING_READ 같은 사유를 남깁니다.
서명만 맞는 반례를 직접 확인하기
실습에는 정상처럼 보이는 결과를 과신하지 않도록 18개의 입력을 넣었습니다. README의 한계 반례 명령으로 추가 입력 15개를 실행하고 expected.json의 이름별 기대값과 비교할 수 있습니다.
- 04_header_only.png는 PNG 서명 8바이트뿐입니다. HEADER_AGREES여도 이미지 본문은 없습니다.
- 05_short.png는 PNG 서명의 마지막 바이트가 없는 7바이트 입력입니다. INCONCLUSIVE입니다.
- 06_prefix_only.pdf는 %PDF-1.7 8바이트뿐이고, 16_uppercase.PDF는 %PDF-XYZ 임의 8바이트입니다. 둘 다 PDF_PREFIX 관측만 남깁니다.
- 07_offset.pdf는 첫 8바이트 뒤에 %PDF-1.7을 넣었습니다. 처음 8바이트에서 보이지 않아 INCONCLUSIVE입니다.
- 09_empty.zip는 빈 ZIP의 시작 표식, 10_marker.zip는 표식 8바이트, 11_broken.zip는 PK 표식 뒤 임의 자료입니다. 모두 제한된 앞부분 대응 결과이며 ZIP 정상성 검사가 아닙니다.
- 12_office_container.xlsx는 ZIP 바이트를 담은 합성 파일입니다. 실제 통합문서가 아니며 .xlsx는 지원하지 않아 읽기 0바이트입니다.
이처럼 같은 상태값 안에도 전체 파일 구조는 크게 다를 수 있습니다. 보고서를 검토할 때 관측 범위와 기대값을 함께 보아야 하는 이유입니다.
기존 결과를 보존하며 다시 실행하기
같은 report_01.json 이름으로 다시 실행하면 STOP FileExistsError와 종료 코드 2가 나옵니다. 결과가 필요하면 --output report_02.json처럼 새 이름을 지정하세요. 기존 결과와 원본은 삭제하지 않습니다. 출력 이름은 .json으로 끝나야 하고, 출력 폴더는 이미 존재해야 합니다. 입력과 출력 경로에 상위 폴더를 뜻하는 .. 구성요소를 사용하지 마세요. 지원 입력의 해당 경로는 내용 읽기 없이 SKIPPED와 PARENT_COMPONENT로 기록하고, 해당 출력 경로는 STOP ValueError와 종료 코드 2로 저장을 거부합니다.
입력을 잘못 적어 READ_ERROR가 나왔다면 samples 하위 파일 이름과 현재 작업 폴더를 확인한 뒤 새 출력 이름으로 다시 실행합니다. 저장 중 디스크 용량이나 권한 문제가 생기면 새 JSON의 일부만 남을 수 있습니다. 그 파일을 완료 결과로 사용하지 말고 원인을 해결한 다음 다른 새 이름으로 실행하세요.
전체 코드는 입력 파일에 쓰기 작업을 하지 않습니다. 다만 읽기로 파일시스템의 접근 시각이 달라질 수 있습니다. 경로와 파일 상태 검사는 악의적인 동시 변경을 막는 보안 경계가 아니므로, 이 실습은 변하지 않는 합성 입력을 별도 연습 폴더에서 다루도록 설계했습니다.
읽는 양과 처리 범위를 제한하기
한 번에 명시한 경로 1~100개를 받으며 파일 이름은 최대 180문자로 제한합니다. 지원 확장자의 일반 파일에서 최대 8바이트를 unbuffered 바이너리 읽기로 요청하므로 100개에서 반환 내용은 최대 800바이트입니다. OS의 선읽기와 실제 물리 디스크 I/O 양은 측정하지 않습니다. 심볼릭 링크, junction, 이들을 포함하는 경로, 폴더와 FIFO는 처리하지 않습니다. 지원 입력에서는 .. 구성요소를 링크 검사보다 먼저 거부해, 경로 정규화로 링크 구성요소가 사라지는 경우도 읽지 않습니다.
JSON은 저장 전에 최대 131,072바이트 제한을 확인합니다. 읽기에는 rb와 buffering=0을, 새 출력에는 xb를 사용합니다. xb는 기존 파일이 있으면 실패하므로 실수로 이전 결과를 덮어쓰지 않습니다. Python open의 바이너리 읽기와 배타 생성
실행 기록과 다운로드 구성
2026년 10월 9일 KST에 Linux CPython 3.12.14에서 코드와 합성 입력을 실행했습니다. 자동시험 17개, 입력 18개의 독립 기대값, 기본 세 파일의 요약, 재실행 충돌과 입력 내용 보존을 확인했습니다. 실제 Windows, Excel, Acrobat, 이미지 뷰어의 열기 동작은 확인하지 않았습니다.
2026년 10월 10일 KST에는 같은 Linux CPython 3.12.14 환경에서 .. 경로 거부 회귀시험 한 개를 추가한 자동시험 18개와 기존 합성 입력 18개의 독립 기대값을 재검증했습니다. 지원 입력의 일반 ../경로와 alias/../경로는 실제 읽기 0바이트로 제외하고, 원본 SHA-256이 그대로임을 확인했습니다. 이 보완에서도 실제 Windows나 Office 앱을 실행한 것은 아닙니다.
실습 ZIP에는 header_audit.py 전체 코드, test_header_audit.py 자동시험, expected.json 기대표, samples의 합성 입력 18개, README와 공식 출처 링크, SHA-256 목록을 넣었습니다. 파일을 실행하거나 ZIP을 추출하는 기능을 검사 코드에 넣지 않았습니다. 독자가 다운로드한 실습 묶음을 별도 폴더에 푸는 과정과 검사 대상 ZIP을 코드로 추출하는 일은 구별합니다.
최초 공식 자료 확인일은 2026년 10월 9일 KST입니다. Python 웹 문서의 3.12 분기는 확인 시 3.12.15로 표시됐고, 실제 시험 버전은 3.12.14입니다. PDF 출처는 2026년 10월 9일 UTC에 RFC 8118 8절과 미국 의회도서관의 형식 설명을 직접 열어 보완했습니다. 이 확인은 정식 ISO 규격 전문 열람이나 PDF 전체 형식 검증을 뜻하지 않습니다.
파일을 열기 전 처음 확인할 질문은 “이 파일은 안전한가”보다 “이름과 지금 관측한 앞부분이 어떤 관계인가”입니다. 이 예제에서 확인한 관계를 근거로 다음 확인을 정하고, 전체 파일 검사 결과로 확대하지 마세요.
실습 파일 내려받기
Python 확장자와 앞 8바이트 실습 ZIP v2 내려받기 (13,969바이트). 2026년 10월 10일 KST 검수본입니다. 전체 코드와 자동시험 18개, 기대표, 합성 입력 18개와 출처를 포함합니다.
다운로드 ZIP의 SHA-256: 3e6cb72f7aae6f90d4495e852ee452009f478dccbd584236acafb569c93dcf94
이 실습은 포함된 합성 입력을 별도 폴더에서 다룹니다. 서명 대응 결과를 파일 정상성이나 안전성 인증으로 확대하지 마세요.