flawopen.com/path-traversal/Python

● CWE-918 · Высокий
Исследования · FlawOpen

Анализ уязвимости: Path Traversal in 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().

💡 Простыми словами (ELI5)

Представьте электронный замок в отеле, запрограммированный открывать только номера на втором этаже. Если гость вводит на панели '../../master-safe', уязвимый замок выходит в коридор и открывает главный сейф управляющего.

Ключевые понятия и термины

Web Application Security
Технический термин (Web Application Security): Core architecture component affected by CWE-918.
CWE-918
Технический термин (CWE-918): Standard Common Weakness Enumeration classification for path-traversal-python.
Defense-in-Depth
Технический термин (Defense-in-Depth): Multi-layered engineering verification and runtime boundary isolation.

Пошаговый сценарий атаки

Step 1

Передача пути к файлу клиентом

Конечная точка принимает имя файла или путь к документу через параметр HTTP-запроса.

Step 2

Внедрение последовательностей обхода каталогов

Злоумышленник передает последовательности '../', '..%2f' или абсолютные пути для выхода из базового каталога.

Step 3

Преодоление границ файловой системы

Бэкенд конкатенирует путь без канонизации и проверки того, что результирующий путь находится внутри базовой папки.

Step 4

Несанкционированное чтение системных файлов

Сервер считывает и передает клиенту конфиденциальные файлы операционной системы (например, /etc/passwd).

Исходный код: Уязвимый vs Защищённый вариант

✕ УЯЗВИМАЯ РЕАЛИЗАЦИЯ
# 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)

Чек-лист по защите системы для инженеров

References