CodeRigor

FREE LESSON

Upload metadata, byte limits & storage keys

Validate a small UTF-8 text-upload policy while separating untrusted display filenames from server-assigned canonical storage keys.

An upload combines several different claims: the user's display filename, the submitted bytes, a declared media type and an eventual storage location. These claims have different authority. A filename can be useful metadata without being allowed to select a directory or overwrite another user's file. A media-type header is a client assertion, not proof of the bytes' content.

This lesson constructs an upload plan in memory. It writes no files, opens no archives and parses no images. The accepted format is deliberately small: nonempty UTF-8 text of at most 1024 bytes with no NUL. A passing check establishes that narrow text policy; it does not establish that an arbitrary file is harmless or malware-free.

Split display metadata from storage identity
start / 3
Only the server-assigned identifier determines the storage key.
Split display metadata from storage identity
Only the server-assigned identifier determines the storage key.

The display filename is a single portable ASCII leaf with a lowercase .txt suffix, a leading letter or digit and a total length of 5–64 characters. The remaining characters may be ASCII letters, digits, dots, underscores and hyphens. Paths, backslashes, colons and header separators are rejected rather than repaired. This restricted grammar is the exercise's policy, not a claim that international filenames are inherently unsafe.

Storage identity comes from a separate server-assigned ID of 32 lowercase hexadecimal characters. The storage key is always uploads/<id>.txt; changing the display filename cannot change that key. The ID is supplied by the trusted fixture. A deployed service needs secure ID allocation, collision handling, access checks and storage permissions. Validation of an ID's spelling does not establish that its source is trustworthy or that it is unpredictable.

Python
text = "é" * 512
print(len(text), len(text.encode("utf-8")))
key = "uploads/" + "a" * 32 + ".txt"
print(key.startswith("uploads/"))

The character count is 512 and the byte count is 1024. A transport or storage limit normally measures bytes, so the build checks byte length before decoding. Invalid UTF-8 is rejected, not replaced with a replacement character. Replacing malformed input would conceal a format error and change the submitted content.

A matching prefix is not a canonical member
start / 2
Download keys must match the entire application-owned grammar, not just its directory-looking prefix.
A matching prefix is not a canonical member
Download keys must match the entire application-owned grammar, not just its directory-looking prefix.

For a download key, a string prefix is insufficient. uploads/../private.txt begins with uploads/ but is not a member of the permitted key grammar. The lab accepts only the exact canonical form produced by the storage-key policy. It does not URL-decode inputs or attempt to normalize arbitrary paths into acceptance.

This canonical key model avoids using user filenames as filesystem paths, but it is not a filesystem containment proof. A real service must account for symlinks, races between checking and opening, object-store permissions and the actual storage API. Authorization is required for downloads even when a key is valid. Serving UTF-8 text also needs correct response metadata and output handling; a text upload can contain HTML-looking characters. Serving it as an attachment with an appropriate media type and nosniff, or encoding it in a view, is a separate integration decision.

The tests include traversal-shaped metadata, malformed bytes, exact size endpoints, shared display names with different storage IDs and a prefix-looking invalid key. Explain what each rejection establishes, and keep the untested filesystem and browser responsibilities visible when moving beyond this local planner.

Watch & read

Free lectures, talks and readings that cover this skill from another angle. All resources →

Revisit 11:48–39:26 for treating stored input as data, then 1:07:51–1:12:44 for server-side checks. File-specific boundaries are taught by the authored lab and OWASP upload reading.