Insecure file upload¶
An upload crosses several boundaries: request parsing, content processing, filesystem storage, and later downloads. A filename extension or a browser-supplied Content-Type is not trustworthy evidence of content. Saving arbitrary uploads in an executable web directory can make a data upload become code execution. Publicly serving active HTML or SVG may instead expose readers to script execution.
OWASP's file upload guidance recommends layered controls including narrow permitted formats, size limits, generated filenames, and appropriate storage and access permissions.
Unsafe example¶
This Python web-handler fragment assumes a framework upload object with a save method. It trusts the name and publishes the content immediately.
upload.save("/srv/app/public/uploads/" + upload.filename)
Safer example: private UTF-8 text attachments only¶
This focused Python helper accepts a binary stream, not a framework-specific upload object. The caller must authenticate the request and pass a trusted, service-owned directory outside the web root. It intentionally does not support PDF, archives, images, or rich text.
import os
from pathlib import Path
from uuid import uuid4
MAX_BYTES = 2 * 1024 * 1024
def store_text_attachment(stream, private_root: Path):
data = bytearray()
while chunk := stream.read(min(65536, MAX_BYTES + 1 - len(data))):
data.extend(chunk)
if len(data) > MAX_BYTES:
raise ValueError("Attachment size is not allowed")
raw = bytes(data)
if not raw or len(raw) > MAX_BYTES:
raise ValueError("Attachment size is not allowed")
text = raw.decode("utf-8", errors="strict")
if "\x00" in text:
raise ValueError("NUL is not allowed in text attachments")
file_id = uuid4().hex
path = private_root / (file_id + ".txt")
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_EXCL, 0o600)
try:
with os.fdopen(fd, "wb") as output:
output.write(raw)
except Exception:
path.unlink(missing_ok=True)
raise
return file_id
The submitted filename never becomes a path. Exclusive creation prevents replacing an existing entry, and the file is not executable or automatically public. Record the generated ID and owner in application storage; serve it only through an authorized download handler. For this policy, use Content-Type: text/plain; charset=utf-8, Content-Disposition: attachment with a server-chosen filename, and X-Content-Type-Options: nosniff. Never render the attachment as trusted HTML.
Limit total request size and multipart field counts before the framework buffers the upload. Add per-user storage quotas, request deadlines, and cleanup of unattached files. If adding other formats, use maintained parsers, format-specific validation and isolated processing; malware scanning or content disarm may be appropriate. Signature checks alone do not prove a document harmless.
Regression test¶
Use a temporary private directory and in-memory streams. Accept a small text fixture. Reject invalid UTF-8, empty data, NUL, and a file one byte over the cap. Verify a client filename such as ../report.txt is never used by the storage API, and test that a second user cannot download the saved attachment. Treat HTML-looking text as an attachment and confirm the download headers prevent the application from treating it as trusted markup.
Related: path traversal, archive extraction, and object authorization.
Reference: CWE-434 — unrestricted upload of a file with a dangerous type.