Flask Path Traversal via open()

HIGH

User input from Flask request parameters flows to open() or io.open() without path sanitization. Use werkzeug secure_filename() and verify the resolved path stays within the intended directory.

Rule Information

Language
Python
Category
Flask
Author
Shivasurya
Shivasurya
Last Updated
2026-03-22
Tags
pythonflaskpath-traversaldirectory-traversalfile-readopenio-opencross-fileinter-proceduraltaint-analysisCWE-22OWASP-A01
CWE References

Interactive Playground

Experiment with the vulnerable code and security rule below. Edit the code to see how the rule detects different vulnerability patterns.

pathfinder scan --ruleset python/PYTHON-FLASK-SEC-007 --project .
1
2
3
4
5
6
7
8
9
rule.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

About This Rule

Understanding the vulnerability and how it is detected

This rule detects path traversal vulnerabilities in Flask applications where user-controlled input from HTTP request parameters flows to Python's open() or io.open() built-in functions without path sanitization. When an attacker supplies directory traversal sequences (../, ..\, URL-encoded variants %2e%2e%2f) in a filename or path parameter, the application reads or writes files outside the intended directory.

Path traversal is one of the most common file-handling bugs in web applications. In Flask apps it typically appears in file download endpoints (open the file, return its contents) and file processing utilities (open the file, parse it). The vulnerability is not always obvious because the user-supplied value may pass through string concatenation with os.path.join() before reaching open() -- os.path.join('uploads/', '../../../etc/passwd') silently produces '../../../etc/passwd' due to a quirk of the join implementation when later segments contain separators.

The rule traces tainted data from Flask request sources to the filename argument of open() and io.open() at position 0. Flows through os.path.basename() or werkzeug's secure_filename() are recognized as sanitizers because basename() strips all directory components and secure_filename() additionally removes unsafe characters that could be used in traversal attempts.

Security Implications

Potential attack scenarios if this vulnerability is exploited

1

Arbitrary File Read

An attacker who supplies ../../../etc/passwd as a filename reads the system password file. On servers where application secrets are stored in files (/etc/ssl/private/server.key, .env, config.ini, ~/.aws/credentials), the attacker reads production secrets in a single request.

2

Source Code Disclosure

Python applications store business logic, database schemas, and hardcoded credentials in .py files. A path traversal from /uploads/ to ../app.py or ../config.py exposes source code that enables more targeted subsequent attacks.

3

Arbitrary File Write (Write Mode)

If the open() call uses write mode ('w', 'a', 'wb'), the attacker can write arbitrary content to arbitrary paths. Writing to /etc/cron.d/ installs a cron job. Writing to the application's own Python files modifies source code. Writing to server configuration files redirects traffic.

4

Log File Poisoning

Traversal to application log files combined with write mode allows log injection -- inserting fake log entries that confuse audit trails, or injecting content into log files that are subsequently parsed by log processing tools.

How to Fix

Recommended remediation steps

  • 1Apply werkzeug.utils.secure_filename() to any user-supplied filename before constructing a file path -- it strips directory separators and traversal sequences.
  • 2After constructing the full path with os.path.join(), resolve symlinks with os.path.realpath() and verify the result starts with the intended base directory prefix.
  • 3Maintain an allowlist of permitted filenames or file extensions rather than relying on path sanitization alone -- reject anything not in the allowlist.
  • 4Separate file storage from the application root directory so a traversal beyond the upload directory cannot reach application source files or server configuration.
  • 5Use send_from_directory() from Flask instead of open() for serving static files -- it performs its own path safety checks.

Detection Scope

How Code Pathfinder analyzes your code for this vulnerability

Scope: global (cross-file taint tracking across the entire project). Sources: Flask HTTP input methods -- request.args.get(), request.form.get(), request.values.get(), request.get_json() -- all of which can deliver attacker-controlled filename or path strings. Sinks: open() and io.open() built-in functions. The .tracks(0) parameter targets argument position 0, the filename/path argument. The mode argument at position 1 (read/write) and other kwargs are not tracked. Sanitizers: os.path.basename() and werkzeug.utils.secure_filename() are recognized as sanitizing transformations. basename() strips all directory components. secure_filename() additionally removes non-ASCII characters and sequences that could be used in path manipulation. A flow through either function is treated as sanitized. The rule follows tainted filenames through variable assignments, string concatenation with os.path.join(), and cross-file function calls to catch traversal chains that pass through utility modules.

Compliance & Standards

Industry frameworks and regulations that require detection of this vulnerability

OWASP Top 10
A01:2021 - Broken Access Control (path traversal is a file access control failure)
CWE Top 25
CWE-22 ranked #8 in 2023 Most Dangerous Software Weaknesses
PCI DSS v4.0
Requirement 6.2.4 -- protect against path traversal and file inclusion attacks
NIST SP 800-53
AC-3: Access Enforcement; SI-10: Information Input Validation
ASVS v4.0
V12.3.1 -- verify that user-submitted filenames are validated or ignored

References

External resources and documentation

Similar Rules

Explore related security rules for Python

Frequently Asked Questions

Common questions about Flask Path Traversal via open()

No. os.path.join('/uploads/', '../../../etc/passwd') returns '../../../etc/passwd' because os.path.join discards earlier components when a later component is an absolute path or begins with a separator. It does not sanitize traversal sequences. Use secure_filename() on the user-supplied part before joining, then verify the resolved absolute path starts with the intended base directory.
os.path.basename() returns only the last component of a path, stripping all directory components. 'secure_filename' from werkzeug does that and also removes leading dots, spaces, special characters, and non-ASCII characters that could cause unexpected behavior on different operating systems. For web application file uploads, secure_filename() is the more robust choice.
basename() alone is sufficient to prevent directory traversal in most cases. However, the realpath() check is also important for symlink attacks: an attacker who can write a symlink into the uploads directory (via another vulnerability) can point it outside the directory. basename() does not protect against this. Always combine basename() or secure_filename() with a realpath() boundary check.
Yes. The rule tracks taint to open() at argument position 0 regardless of the mode argument. Both read and write opens with tainted paths are flagged. Write mode path traversal is often more severe because it allows file creation and content modification.
Cloud storage uses key names rather than filesystem paths. Key injection (../config or arbitrary key names) is a related but separate issue. If you pass user-supplied values as S3 key names, ensure the key is constrained to a prefix using startswith() checks. This rule focuses on Python's open() and io.open() for local filesystem operations.
Use Flask's send_from_directory(directory, filename) function. It validates that the requested file is within the specified directory before serving it. Pass the upload directory as the first argument and the sanitized filename (from secure_filename()) as the second. Do not call open() manually for serving files.
secure_filename() returns an empty string when the input filename contains only unsafe characters (e.g., '../../../etc/passwd' after stripping becomes empty). Always check for an empty return value and reject the request with a 400 error. Never use an empty string as a file path.

New feature

Get these findings posted directly on your GitHub pull requests

The Flask Path Traversal via open() rule runs in CI and posts inline review comments on the exact lines — no dashboard, no SARIF viewer.

See how it works