Files

3.6 KiB

Fileserver security model

The rule everything else follows

Uploaded content is attacker-controlled. Every guard below exists because some part of the request — the filename, the declared media type, the range, the offset — is a value the caller chose.

Path safety

A client value never becomes a path. The physical key is server-generated, and ContentKey's character class excludes . entirely, so no traversal or extension-shaped segment survives validation. DefaultPhysicalPathResolver is the only place an identifier becomes a Path, and it normalizes and re-checks containment after construction rather than trusting the input.

Symlink refusal happens at open time, not only at construction. A parent directory can be replaced between the two, so a check that ran only at path-building time would be a race, not a guard.

Filename handling

OriginalFilenamePolicy strips path separators, NUL, quoting characters, and the colon — the last because on Windows it opens both a drive reference and an NTFS alternate data stream, so a name that keeps it is still path-shaped after the slashes are gone. Control characters and bidirectional overrides are removed, dot runs collapsed, reserved device names guarded, and the result is bounded in UTF-8 bytes.

The sanitized name is display data. It is never used to build a key, and it reaches a header only through ContentDispositionFactory, which restricts the ASCII form and percent-encodes the UTF-8 form.

Content type

The client's Content-Type is stored as a claim. The verified type comes from the verification pipeline, and only the verified type is served. A claimed type that contradicts the content is quarantined rather than corrected.

Scriptable types are never served inline, whatever the caller asked for: serving stored HTML or SVG inline from an upload origin is a stored cross-site scripting primitive. Every download also carries X-Content-Type-Options: nosniff.

Verification precedence

REJECT > QUARANTINE > RETRY > ACCEPT. A verifier that times out or throws is RETRY, never a silent pass, and an empty verifier chain answers RETRY rather than accepting. A file becomes publicly readable only after an ACCEPT.

Range safety

The range budget is enforced before content is opened, so a request naming many ranges is rejected without amplifying into storage work. An unsatisfiable range answers 416 with the real length and opens nothing.

Authorization

Every public operation calls the injected FileAccessPolicy before any quota reservation or storage mutation, so a denial leaves no record, no reservation, and no staging object. Startup refuses to run a production profile with an allow-all policy.

Delegation

X-Accel-Redirect is emitted only after authorization and the READY gate, and only for a full, unconditional response. The internal prefix must be an internal Nginx location; the front proxy also strips any client-supplied delegation header so a caller cannot name an internal object.

Telemetry

No metric label, span attribute, or audit record carries a file id, upload id, filename, path, or user id. Where correlation is needed the value is a keyed HMAC fingerprint — keyed because the identifier space is enumerable and an unkeyed digest of it is reversible by brute force.

Ambiguous failures

A failure whose operation may already have taken effect is reported as ambiguous and is never retryable. On a network filesystem a lost response is indistinguishable from a rejection at the socket level, so anything not provably safe is treated as ambiguous and sent to reconciliation.