Path traversal

Prevent user-controlled paths from reaching files outside the intended base directory.

Description

Path traversal uses input to manipulate filesystem paths and access files outside the intended file or directory scope. Attackers may use relative paths such as ../ to reach sensitive system files.

Potential impact

  • Information disclosure: An attacker may read sensitive system files and obtain important information.
  • Data damage: An attacker may overwrite or delete system files.
  • Privilege escalation: Modifying important files may enable an attacker to gain additional privileges.

Remediation

  • Validate filenames and paths. Do not rely only on blocking the string ../; check that the actual accessed path remains inside the permitted directory.
  • Restrict access to files within an allow-listed directory.
  • In Flask/Werkzeug, prefer APIs designed to help prevent path traversal, such as secure_filename(), safe_join() and send_from_directory().
  • If only a filename is needed, extract the final component with os.path.basename() or pathlib.PurePath(...).name, and always combine it with a trusted base directory.

Examples

open

Before

python
# Unsafe Flask code
from flask import Flask, request

app = Flask(__name__)

@app.route('/read')
def unsafe_read():
    filename = request.args.get('filename')
    with open(filename, 'r') as file:
        content = file.read()
    return content

After

python
# Safe Flask code
from flask import Flask, request, abort
from pathlib import Path
from werkzeug.security import safe_join
from werkzeug.utils import secure_filename

app = Flask(__name__)
# Server-managed directory that attackers cannot modify
BASE_DIR = Path("/safe/directory").resolve(strict=True)

@app.route('/read')
def safe_read():
    filename = secure_filename(request.args.get('filename', ''))
    if not filename:
        return abort(400, description="Invalid file path")

    candidate = safe_join(str(BASE_DIR), filename)
    if candidate is None:
        return abort(400, description="Invalid file path")

    try:
        fullpath = Path(candidate).resolve(strict=True)
        fullpath.relative_to(BASE_DIR)
    except FileNotFoundError:
        return abort(404, description="File not found")
    except ValueError:
        return abort(400, description="Invalid file path")

    if not fullpath.is_file():
        return abort(404, description="File not found")
    with open(fullpath, 'r') as file:
        content = file.read()
    return content

Explanation:

  • Before: Uses unvalidated input as a path, allowing an attacker to reach sensitive files with relative paths such as ../.
  • After: Rejects empty names and restricts path components with secure_filename() and safe_join(). It then uses Path.resolve(strict=True) to resolve symbolic links and checks that the actual file is inside a base directory that attackers cannot modify.

Using only the filename component

python
import os
from pathlib import PurePath

def filename_only(raw_name):
    if not isinstance(raw_name, str):
        raise TypeError("filename must be a string")
    # Remove directory components using either method
    name = os.path.basename(raw_name)
    # name = PurePath(raw_name).name
    if name in {"", ".", ".."}:
        raise ValueError("invalid filename")
    return name

basename and PurePath.name extract only the last path component. When opening or sending a file, combine this value with a trusted base directory too.

Flask send_file

Before

python
# Unsafe Flask send_file
from flask import Flask, request, send_file

app = Flask(__name__)

@app.route('/read')
def unsafe_read():
    filename = request.args.get('filename')
    return send_file(filename)

After

python
# Safe Flask send_from_directory
from flask import Flask, request, send_from_directory
from pathlib import Path
from werkzeug.utils import secure_filename

app = Flask(__name__)
BASE_DIR = Path("/safe/directory").resolve(strict=True)

@app.route('/read')
def safe_read():
    filename = secure_filename(request.args.get('filename', ''))
    if not filename:
        return "Invalid file path", 400
    return send_from_directory(BASE_DIR, filename, as_attachment=True)

Explanation:

  • Before: Uses unvalidated input as a path, allowing an attacker to reach sensitive files with relative paths such as ../.
  • After: Uses send_from_directory to restrict access to the specified directory, rejects empty names and restricts the filename with secure_filename(). The server must manage the base directory so attackers cannot add files or symbolic links.

References