Snapshot of the in-flight state that already existed, identically, in both this worktree and the main checkout before this session began: the initial HTTP Client platform implementation (previously untracked), the redis-lab removal, and the JPA / object-storage / notification integration work. Kept separate from this session's HTTP Client review response, which lands in the following commit, so the two bodies of work stay reviewable apart. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
75 lines
3.6 KiB
Markdown
75 lines
3.6 KiB
Markdown
# 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.
|