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.