Safeguard
Application Security•

CWE-73 External Control of File Name or Path: Find and Fix

When user input decides which file your code opens, attackers pick the file. This guide covers CWE-73, with a vulnerable Python pattern and the fixes MITRE recommends.

Safeguard Security Team
5 min read

CWE-73 is what happens when a product lets user input control or influence the paths or file names used in file system operations. It is broad on purpose. Path traversal, remote file inclusion and several other bugs are specific forms of it, and many real flaws are chains that start here.

Any application that reads, writes, includes or deletes a file based on something a user sent is exposed: download endpoints, template or theme selectors, log viewers, import tools and file managers.

How it happens

MITRE describes the pattern as two conditions. An attacker can specify a path used in a file system operation, and by specifying that resource the attacker gains a capability they would not otherwise have. For example, the program may let the attacker overwrite the chosen file or run with a configuration the attacker controls.

A typical vulnerable shape in Python looks like this:

# Vulnerable: the user decides the file name
@app.route("/download")
def download():
    name = request.args["file"]
    return send_file("/srv/app/reports/" + name)

A value like ../../etc/passwd walks out of the reports directory. Even without traversal, a name pointing at a file the user should not see is enough.

The observed examples show how many forms this takes. CVE-2022-45918 is a chain where a learning management tool debugger uses external input to locate previous session logs and does not validate the path, allowing file system path traversal. CVE-2008-5748 describes external control of the user's chosen language and theme enabling path traversal, and CVE-2008-5764 describes control of the target language enabling remote file inclusion. Their Gold pages are CVE-2022-45918, CVE-2008-5748 and CVE-2008-5764.

What an attacker gains

The MITRE record lists three groups of impact.

  • Integrity and confidentiality: reading or modifying files. Confidentiality is violated when the targeted file is not directly readable by the attacker.
  • Integrity, confidentiality and availability: the application can operate on unexpected files, and if the file is a program or other executable code, the result can be execution of unauthorized code or commands.
  • Availability: an unexpected file that the application modifies, or a file name that triggers a crash or resource exhaustion.

How to find it

  • Static analysis that models data flow can often detect external control of file names. MITRE warns it may not recognize when proper validation is happening, which leads to false positives, so review the results.
  • Manual testing counts. MITRE recommends penetration testing, threat modeling and interactive tools that let a tester record and modify an active session, especially for weaknesses tied to design and business rules.
  • A quick grep for file open, read, write, include and delete calls whose argument contains request data is a cheap first pass.

Click through it at your own pace, or open it full screen.

How to fix it

MITRE's guidance, in plain steps:

  1. Map instead of accept. When the set of file names is limited or known, map fixed input values such as numeric IDs to the real file names and reject everything else. ID 1 could map to "inbox.txt" and ID 2 to "profile.txt".
  2. Use an allowlist for names. Limit the character set. If feasible, allow only a single "." in the file name and exclude directory separators such as "/". Use a list of allowed file extensions. Do not rely only on filtering out dangerous characters, because that is a denylist and can be incomplete. Filtering "/" is not enough if the file system also accepts "".
  3. Canonicalize. Use a built-in path canonicalization function such as realpath() in C, which removes ".." sequences and symbolic links.
  4. Repeat client checks on the server. Anything checked in the browser can be changed by the attacker.
  5. Limit the blast radius. Run in a jail or sandbox such as chroot or AppArmor, and use OS permissions with a low-privileged user. MITRE notes a jail only limits the impact to the operating system and may not be feasible everywhere.

The safe version of the earlier route combines a mapping with a canonical path check:

import os

REPORTS = {"1": "inbox.txt", "2": "profile.txt"}
BASE = os.path.realpath("/srv/app/reports")

@app.route("/download")
def download():
    name = REPORTS.get(request.args.get("id", ""))
    if name is None:
        abort(404)
    path = os.path.realpath(os.path.join(BASE, name))
    if os.path.commonpath([BASE, path]) != BASE:
        abort(404)
    return send_file(path)

The mapping already makes traversal impossible. The canonical check is the second lock in case the table ever grows or changes.

Quick check

Why is removing "../" from input not a complete fix?

Sequential filtering can still leave a dangerous string. MITRE's example is the string ".../...//", where removing two "../" sequences leaves "../" behind. Allowlists and canonicalization are sturdier.

What is the strongest simple control when the file set is known?

Map fixed IDs to real file names and reject all other input.

Sources

Never miss an update

Weekly insights on software supply chain security, delivered to your inbox.

Self-healing security runs on Safeguard.

Your first fix PR is minutes away.

No sales call required, even your agent can complete the purchase over MCP.