파일 업로드 취약점

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로 저장
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 연관성

분류항목파일 업로드와의 연관
A03Injection웹쉘(.php) 업로드 → 서버에서 임의 코드 실행
A05Security Misconfiguration확장자 검증 미설정, 업로드 디렉토리 권한 오설정
A01Broken 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 400
OWASP A03(Injection), A05(Misconfiguration)에 해당하는 고위험 취약점