파일 업로드 취약점
OWASP A03 · ADVANCED
파일 업로드 취약점 완전 정복
허용목록(Allow-list)과 파일명 재생성(UUID)으로 웹쉘 업로드를 원천 차단하는 방법
OWASP A03: Injection
OWASP A05: Security Misconfiguration
Flask · Python
Path Traversal
1
파일 업로드가 왜 위험한가?
파일 업로드 기능은 공격자에게 서버 코드 실행 권한을 넘겨줄 수 있는 치명적인 취약점입니다. 두 가지 핵심 공격 벡터가 존재합니다.
공격 벡터 1 — 웹쉘(Web Shell) 업로드
확장자 검증 없이
shell.php를 업로드하면, 공격자가 해당 URL에 접근해 서버에서 임의 명령을 실행할 수 있다.공격 벡터 2 — Path Traversal
원본 파일명을 그대로 사용하면
../../etc/passwd 같은 경로로 서버 내부 파일을 덮어쓰거나 민감한 위치에 저장할 수 있다.웹쉘 업로드 공격 플로우
STEP 1
shell.php 생성
→
STEP 2
파일 업로드
→
STEP 3
/uploads/shell.php 접근
→
결과
서버 장악
2
핵심 방어 개념 2가지
개념 1 — 허용목록(Allow-list) vs 블랙리스트
블랙리스트 (위험)
"
→
.php만 막는다"→
.php5, .phtml, .phar로 우회 가능VS
허용목록 (권장)
"
→ 목록에 없는 모든 확장자 자동 차단
.jpg / .jpeg / .png / .gif만 허용"→ 목록에 없는 모든 확장자 자동 차단
| 방식 | 예시 | 우회 가능? | 결론 |
|---|---|---|---|
| 블랙리스트 | .php 차단 | 가능 | .php5, .phtml로 우회 |
| 허용목록 | .jpg/.png/.gif만 허용 | 불가 | 새로운 우회 기법에도 차단 |
개념 2 — 파일명 재생성 (UUID)
원본 파일명 사용 (위험)
../../etc/passwd→ 서버 루트의 민감한 파일 덮어씀
→
UUID 재생성 (안전)
7c0c19d3...cbd.png→ 예측 불가능한 이름, Path Traversal 원천 차단
3
취약한 코드 vs 방어 코드 비교
취약한 코드
upload_app_bad.py
PYTHON
# 취약한 코드
@app.post("/upload")
def upload():
f = request.files.get("file")
# 확장자 검증 없음
# 원본 파일명 그대로 사용
f.save("./uploads/" + f.filename)
# shell.php → 실행 가능!
# ../../etc/passwd → 덮어쓰기!
방어 코드
upload_app.py
PYTHON
# 방어 코드
@app.post("/upload")
def upload():
f = request.files.get("file")
ext = os.path.splitext(
f.filename or "")[1].lower()
# TODO (1) — 허용목록 검증
if ext not in ALLOWED_EXT:
abort(400, "extension not allowed")
# TODO (2) — UUID 파일명 재생성
safe_name = uuid.uuid4().hex + ext
f.save(str(UPLOAD_DIR / safe_name))
완성 코드 전체
upload_app.py
PYTHON
import os, uuid
from pathlib import Path
from flask import Flask, request, abort, jsonify
UPLOAD_DIR = Path("./uploads").resolve()
UPLOAD_DIR.mkdir(exist_ok=True)
# ① 허용목록 — 점 포함, 소문자로 통일
ALLOWED_EXT = {".jpg", ".jpeg", ".png", ".gif"}
app = Flask(__name__)
app.config["MAX_CONTENT_LENGTH"] = 5 * 1024 * 1024 # 5MB
@app.post("/upload")
def upload():
f = request.files.get("file")
if not f:
abort(400, "no file")
original_name = f.filename or ""
ext = os.path.splitext(original_name)[1].lower()
# ② TODO (1) — 확장자 허용목록 검증
if ext not in ALLOWED_EXT:
abort(400, "extension not allowed")
# ③ TODO (2) — UUID로 파일명 재생성
safe_name = uuid.uuid4().hex + ext
target = UPLOAD_DIR / safe_name
f.save(str(target))
return jsonify(name=safe_name, size=target.stat().st_size)
if __name__ == "__main__":
app.run(host="127.0.0.1", port=5002)
4
실행 결과 — 4가지 케이스
good.png
200
허용목록 통과
UUID로 저장
UUID로 저장
shell.php
400
.php 비허용
즉시 차단
즉시 차단
../../etc/passwd
400
확장자 없음
차단
차단
noext
400
확장자 없음
차단
차단
$ printf '\x89PNG\r\n\x1a\nHELLO' > good.png
$ curl -s -F "file=@good.png" http://127.0.0.1:5002/upload
{"name":"7c0c19d352d14e198571ae2707831cbd.png","size":13} HTTP=200
$ printf '<?php phpinfo(); ?>' > shell.php
$ curl -s -F "file=@shell.php" http://127.0.0.1:5002/upload
<p>extension not allowed</p> HTTP=400
$ ls uploads/
7c0c19d352d14e198571ae2707831cbd.png ← UUID 이름만 존재
5
핵심 개념 Q&A
Q1
사용자가 보낸 파일명을 그대로 쓰면 안 되는 이유가 뭔가요?
A
../../etc/passwd 같은 경로 순회(Path Traversal) 공격으로 서버 내 임의의 위치에 악성 파일을 저장하거나 민감한 파일을 덮어쓸 수 있기 때문입니다. uuid4().hex + ext로 재생성하면 파일명 자체가 예측 불가능해져 공격이 원천 차단됩니다.Q2
블랙리스트보다 허용목록(Allow-list)이 왜 더 안전한가요?
A
블랙리스트는
.php5, .phtml, .phar처럼 예상하지 못한 우회 확장자가 존재하면 뚫립니다. 허용목록은 명시적으로 허가된 확장자(.jpg/.png/.gif)만 통과시키므로, 아직 알려지지 않은 새로운 우회 확장자에도 기본적으로 차단됩니다.6
실무에서 추가로 적용하는 방어 기법
🔬
Magic Byte 검사
확장자가 .png여도 실제 파일 헤더(magic number)가 PNG 시그니처인지 검증. GIF87a로 위장한 PHP 파일 차단 가능.
🗄️
웹 루트 외부 저장
업로드 파일을 웹 루트(public/) 밖이나 S3 같은 오브젝트 스토리지에 저장. URL로 직접 실행 불가능.
🛡️
바이러스 스캐닝
ClamAV 등의 도구로 업로드 시점에 실시간 악성코드 스캔. 알려진 웹쉘 패턴 탐지 가능.
OWASP Top 10 연관성
| 분류 | 항목 | 파일 업로드와의 연관 |
|---|---|---|
| A03 | Injection | 웹쉘(.php) 업로드 → 서버에서 임의 코드 실행 |
| A05 | Security Misconfiguration | 확장자 검증 미설정, 업로드 디렉토리 권한 오설정 |
| A01 | Broken Access Control | 업로드된 파일을 인증 없이 직접 URL 접근 허용 |
7
파일 업로드 보안 체크리스트
-
허용목록(Allow-list) 적용 —
.jpg / .jpeg / .png / .gif처럼 명시적으로 허가된 확장자만 통과시킨다. -
파일명 UUID 재생성 —
uuid4().hex + ext로 원본 파일명을 완전히 폐기한다. -
파일 크기 제한 —
MAX_CONTENT_LENGTH로 DoS 공격을 방지한다. -
웹 루트 외부 저장 — 업로드 디렉토리가 웹 서버의 public 영역 밖에 위치하도록 설정한다.
-
Magic Byte 검사 (실무 권장) — 확장자뿐 아니라 실제 파일 헤더를 검증한다.
핵심 요약
블랙리스트는
.php5 / .phtml 우회에 뚫린다 → 허용목록(Allow-list)만 신뢰원본 파일명 그대로 사용 = Path Traversal 위험 → UUID로 재생성 필수
정상 케이스:
HTTP 200 + UUID 파일명 / 악성 케이스: HTTP 400OWASP A03(Injection), A05(Misconfiguration)에 해당하는 고위험 취약점
'보안 공부' 카테고리의 다른 글
| 악성 코드 분석 - 악성 코드 분석 개요 (0) | 2026.05.28 |
|---|---|
| 서버 관리자가 실수하기 쉬운SSH 설정 5가지 알아보기 (0) | 2026.05.11 |
| 크로스 사이트 스크립팅 (Cross-Site Scripting, XSS) (0) | 2026.05.06 |
| 웹사이트 보안 헤더, Python 3줄로 점검하기 (0) | 2026.05.04 |
| Insecure Direct Object Reference (1) | 2026.05.03 |