Repository navigation
fix(server): serve only the public namespace from the static files route - #1837
Open
ethicnology-agent wants to merge 1 commit into
Open
ethicnology-agent wants to merge 1 commit into
ethicnology-agent wants to merge 1 commit into
Conversation
publicLocalFilePath split public from private with a deny-list holding one rule: refuse sessions/<id>/avatar/<file>. Everything else under the shared storage root was returned, and GET /files/* has no authentication, so in local-storage deployments session attachments and project avatars were served to anyone who knew or guessed the path — bypassing the ownership checks their own authenticated routes perform on the same bytes. The single rule also compared segments case-sensitively while the open() that followed did not, so sessions/<id>/AVATAR/<file> reached the one object the rule existed to protect on a case-insensitive filesystem. Invert it: return a path only when its first segment is public/, the namespace uploadImage writes and the only tree meant to be readable without a bearer token. A namespace added later is then refused by default instead of being exposed until someone remembers to extend the list. Confidentiality of those blobs still rests on client-side encryption; this restores the access control that was supposed to sit in front of it.
ethicnology-agent
force-pushed
the
fix/server-files-route-allowlist
branch
from
October 1, 2026 19:02
6595e17 to
26cdb95
Compare
Author
|
Rebased onto current |
This branch has not been deployed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes the
GET /files/*defect reported in #1832's sibling, #1830 — filed as part of the/files/*finding in the audit issues (#1829–#1833).Problem
publicLocalFilePathsplits public from private with a deny-list holding exactly one rule:GET /files/*(api.ts:84-98, registered wheneverisLocalStorage()) has no authentication. So every namespace under the shared storage root that the rule does not name was served to any anonymous caller who knew the path — including session attachments (sessions/<id>/attachments/<uuid>.enc) and project avatars (projects/<id>/avatar/<uuid>.enc), whose own authenticated routes do check ownership before handing over the same bytes.The single rule also compares segments case-sensitively while the
open()that follows does not, sosessions/<id>/AVATAR/<file>reached the one object the rule existed to protect on a case-insensitive filesystem — which is the default on macOS.What this changes
Invert the filter. A path is returned only when its first segment is
public/, the namespaceuploadImagewrites (public/users/<userId>/<directory>/<filename>) and the only tree meant to be readable without a bearer token. A namespace added later is then refused by default instead of being exposed until someone remembers to extend the list.Four cases in the existing spec, verified red/green:
sessions/<id>/attachments/…andsessions/<id>/AVATAR/…both return a path where the test expectsnull;public/users/u1/avatars/a.webp.tsc --noEmit -p tsconfig.build.jsonis clean and the server suite passes 123/123.Scope of the impact, stated honestly
This is not a break of confidentiality on its own: the blobs are client-side encrypted, the filenames are random UUIDs, and no route hands one account another account's ref. It is the access control that was meant to sit in front of that encryption, and it was open. Treat it as defence in depth restored, not as a disclosure.
It affects local-storage deployments only —
happy serverand the standalone Docker image. S3-backed deployments never register the route.What this deliberately does not change
public/.path.resolvedoes not resolve them, so a link planted in that tree would still escape. The server writes that tree itself, so there is no untrusted writer today; worth hardening separately rather than widening this diff.Content-TypeandX-Content-Type-Optionson the route's responses. Reported separately in the audit issues; it needs a decision about what to send, not a filter change.