Safeguard
Application Security•

CWE-646 Reliance on File Name or Extension of Uploaded Files

Trusting a filename or extension to decide how an uploaded file is handled lets attackers disguise code as an image. Decide by content on the server.

Safeguard Security Team
5 min read

CWE-646, Reliance on File Name or Extension of Externally-Supplied File, covers upload handling that decides what to do with a file based on its name or extension. Attackers can cause the file to be misclassified and processed in a dangerous way. It hits web applications and servers that accept uploads: avatars, attachments, imports, documents.

How it happens

A name is text the uploader chooses. It says what the uploader claims the file is. Code that routes, stores or serves a file according to that claim hands the decision to the attacker.

MITRE describes the pattern: an application may use the name or extension of a user-supplied file to select the process that handles it, decide what data to make available, or decide which resources to allocate. If the attacker makes the code misclassify the file, the wrong action occurs. The example in the record is a file ending in .php.gif that looks like a GIF image but is processed as PHP code.

# Vulnerable: extension decides trust
ALLOWED = (".gif", ".png", ".jpg")

def accept(upload):
    if upload.filename.lower().endswith(ALLOWED):
        upload.save(os.path.join(WEB_ROOT, "uploads", upload.filename))

The check looks reasonable and still fails in several ways. A name like shell.php.gif ends in an allowed extension, and some server setups handle double extensions in unexpected ways. A file with the right name can contain anything. The Content-Type header sent by the client is just as free to forge as the name.

There is also a second problem in the code above: the file is saved under the user's chosen name inside the web root, so if the server executes it, the attacker wins.

Related weaknesses in the CWE catalog cover these neighbors. Unrestricted file upload and path traversal often show up in the same code, and the MITRE page for CWE-637 cites a case where checking only the Content-Type enabled unrestricted file upload.

What an attacker gains

MITRE lists three consequences:

  • Reading application data: an attacker may be able to read sensitive data.
  • Denial of service by crash, exit or restart.
  • Gaining privileges or assuming identity.

MITRE adds that in extreme cases code execution is possible, though the attacker may also cause other misbehavior, depending on which action the code takes for the misclassified file.

How to find it

The record lists no detection methods and no observed CVEs, so rely on review and testing:

  1. Find every upload handler and read what decides the file's fate: the name, the extension, the declared type or the content.
  2. Upload files with mismatched names and content. A text file named .png, a script named .gif, a file with a double extension.
  3. Upload a file whose name contains path characters, spaces, trailing dots and unusual case.
  4. Check where files land and whether the web server can execute from that location.
  5. After upload, request the file and see how the server serves it.

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

How to fix it

MITRE's mitigation: make decisions on the server side based on file content, not on file name or extension.

That is the core rule. In practice:

  • Inspect the content. Check the file signature (magic bytes) or, better, parse the file with a library for the expected format and reject files that fail. For images, re-encoding the image through an imaging library drops hidden payloads.
  • Generate the stored name on the server and ignore the supplied one. Keep the original name only as display text and encode it when you show it.
  • Store uploads outside the web root, or on a separate domain or storage service that never executes content.
  • Serve files with a fixed, correct Content-Type and a download disposition when they should not render.
  • Configure the web server so that upload directories never run scripts.
import uuid
from PIL import Image

def accept_image(upload):
    try:
        img = Image.open(upload.stream)
        img.verify()
    except Exception:
        raise Rejected("not a valid image")
    upload.stream.seek(0)
    name = uuid.uuid4().hex + ".png"
    Image.open(upload.stream).convert("RGB").save(os.path.join(STORE_DIR, name), "PNG")
    return name

Here the decision rests on whether the content parses as an image, and the name comes from the server. If your file handling uses libraries, such as image or archive parsers, keep them updated. The in-browser dependency scan at gold.safeguard.sh/scan needs no login, keeps the file on your machine and covers up to 100 dependencies.

Quick check

Why is a .php.gif file a problem?

It looks like a GIF by its name, but some server configurations process it as PHP code, so a name check is fooled.

Is the Content-Type header a better signal?

Not by itself. The client supplies it, so it is as easy to forge as the file name.

What does MITRE recommend instead?

Make decisions on the server side based on the file content, not the file name or extension.

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.