flawopen.com/path-traversal/Python
How os.path.join silently discards the root directory when input starts with a slash, and how to use pathlib.Path.resolve() and is_relative_to().
호텔 2층 객실만 열 수 있도록 프로그래밍된 도어락을 비유로 들어보겠습니다. 만약 투숙객이 키패드에 '../../master-safe'를 입력하면, 결함이 있는 잠금장치가 복도를 벗어나 지배인의 메인 금고 문을 열어버리는 현상입니다.
Web Application SecurityCWE-918.CWE-918CWE-918): Standard Common Weakness Enumeration classification for path-traversal-python.Defense-in-Depth엔드포인트가 HTTP 매개변수를 통해 사용자가 지정한 파일명이나 리소스 경로를 전달받습니다.
공격자가 파일명에 '../', '..%2f' 등의 상대 경로 또는 절대 경로 우회 문자열을 삽입합니다.
백엔드가 표준화(Canonical) 경로 검증 없이 기본 디렉토리와 문자열을 결합하여 경계를 이탈합니다.
서버가 /etc/passwd, 환경 변수 파일 등 민감한 시스템 파일을 읽어 공격자에게 반환합니다.
# VULNERABLE: os.path.join discards BASE_DIR if filename starts with '/'
import os
from flask import Flask, request, abort, send_file
app = Flask(__name__)
BASE_DIR = '/var/app/public/user_uploads'
@app.route('/download')
def download_file():
filename = request.args.get('file')
# If filename is "/etc/passwd", os.path.join returns "/etc/passwd"!
# If filename is "../../etc/passwd", it traverses outside BASE_DIR
target_path = os.path.join(BASE_DIR, filename)
if not os.path.exists(target_path):
abort(404)
return send_file(target_path)
# HARDENED: Resolve canonical path and enforce strict boundary containment
from pathlib import Path
from flask import Flask, request, abort, send_file
app = Flask(__name__)
BASE_DIR = Path('/var/app/public/user_uploads').resolve()
@app.route('/download')
def download_file():
filename = request.args.get('file', '')
# 1. Strip leading slashes and resolve relative components
clean_name = filename.lstrip('/\\')
target_path = (BASE_DIR / clean_name).resolve()
# 2. Strict containment check: target MUST be inside BASE_DIR
if not target_path.is_relative_to(BASE_DIR) or not target_path.is_file():
abort(403)
return send_file(target_path)
pathlib.Path.resolve() rather than os.path.abspath()을(를) 사용하십시오.