Skip to content
Threats Guide

safe_file_access.py

Safe file access: stop path traversal and file inclusion

  • prevention
  • python
  • 64 lines

Run security tools only on systems you own or have written permission to test.

Run it

From the repository root, after pip install -r tools/requirements.txt
cd 02-web-application-security/file-inclusion/prevention
python3 safe_file_access.py --help

Source

View on GitHub
#!/usr/bin/env python3"""Safe file access: stop path traversal and file inclusionLocation: 02-web-application-security/file-inclusion/prevention/safe_file_access.pyThree layers, strongest first:  1. allowlist_lookup(): never build a path from user input. Map a short key ("terms") to a fixed     file and reject everything else.  2. safe_join(): when a user-supplied name is unavoidable, resolve the final real path (following     symlinks and ../ ) and require it to stay inside the base directory.  3. allowed extensions and null-byte rejection.Checking for the text "../" is NOT a defence: attackers use ..%2f, ....//, backslashes anddouble encoding. Always resolve the path and compare it to the base directory.""" import os  class UnsafePath(ValueError):    pass  def allowlist_lookup(key, mapping):    """mapping example: {"terms": "/srv/app/pages/terms.html", "privacy": "/srv/app/pages/privacy.html"}"""    try:        return mapping[key]    except (KeyError, TypeError):        raise UnsafePath(f"unknown resource: {key!r}")  def safe_join(base_dir, user_path, allowed_extensions=None):    if "\x00" in user_path:        raise UnsafePath("null byte in path")    if os.path.isabs(user_path) or user_path.startswith(("\\", "/")) or (len(user_path) > 1 and user_path[1] == ":"):        raise UnsafePath("absolute paths are not allowed")    base = os.path.realpath(base_dir)    candidate = os.path.realpath(os.path.join(base, user_path))   # resolves .., symlinks    if os.path.commonpath([base, candidate]) != base:        raise UnsafePath("path escapes the base directory")    if allowed_extensions and os.path.splitext(candidate)[1].lower() not in allowed_extensions:        raise UnsafePath("file type not allowed")    return candidate  def read_file(base_dir, user_path, allowed_extensions=(".txt", ".html", ".md")):    path = safe_join(base_dir, user_path, allowed_extensions)    with open(path, "rb") as handle:        return handle.read()  if __name__ == "__main__":    import tempfile    with tempfile.TemporaryDirectory() as base:        open(os.path.join(base, "report.txt"), "w").write("quarterly report")        for attempt in ["report.txt", "../etc/passwd", "/etc/passwd", "report.txt\x00.png", "sub/../report.txt", "..%2f..%2fetc/passwd"]:            try:                print(f"{attempt!r:34} -> OK, read {len(read_file(base, attempt))} bytes")            except UnsafePath as exc:                print(f"{attempt!r:34} -> blocked: {exc}")            except OSError as exc:                print(f"{attempt!r:34} -> not found")