flawopen.com/Path Traversal/Python
Imagine an employee badge scanner that unlocks 'Room A/closet'. If a visitor inputs '/master-safe', Python's os.path.join sees the leading slash, assumes the visitor gave an absolute master key, and throws away 'Room A' entirely, opening the master safe.
In Python, developers often rely on os.path.join(UPLOAD_DIR, user_file). However, POSIX path concatenation rules state that if an argument begins with a slash (/etc/passwd), all previous path components are discarded. Additionally, directory traversal sequences (../../) are not evaluated by os.path.join until resolved against the filesystem.
In 2022, CVE-2007-4559 resurfaced across over 350,000 open-source repositories when security researchers demonstrated that naive tarfile and zipfile extractions in Python allowed attackers to overwrite system binaries via relative path members.
CISA Cybersecurity Advisory & MITRE CVE repository.# 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)
Using pathlib.Path.resolve() resolves all intermediate symbolic links and .. segments to an absolute filesystem path. Calling target_path.is_relative_to(BASE_DIR) mathematically verifies that the canonical path starts with the base folder path, preventing any breakout.
os.path.abspath only eliminates '.' and '..'; it does not resolve symbolic links on disk. An attacker can create a symlink pointing to /etc/shadow. Use Path.resolve() or os.path.realpath().
Python's tarfile.extractall() historically did not sanitize member names. In Python 3.12+, always specify filter='data'.
Attackers bypass naive string replacement using nested sequences (....//), URL encoding (%2e%2e%2f), or absolute paths (/etc/passwd). Canonicalization is mandatory.
grep -rn "os\.path\.join" --include="*.py" .
grep -rn "tarfile\.extractall" --include="*.py" .
# Bandit SAST rule: bandit -r . -t B108,B202
pathlib.Path.resolve() rather than os.path.abspath()target.is_relative_to(base_dir) before any read, write, or stat operationtarfile.extractall(filter='data') to enforce PEP 706 extraction safetyThe POSIX standard defines path joining such that an absolute path parameter resets the root. Python adhered strictly to this standard, making it a common trap for web developers.