Summary
GET /api/storage/* serves any object whose key is known. It does not enforce object ownership, visibility, prefix policy, or signed read authorization. The default public URL builder points to this route, presigned GET URLs last 48 hours, and storage keys include only 8 UUID characters plus the original sanitized filename.
The proxy path also stores full S3 objects in an unbounded in-memory Map.
Evidence
- apps/web/src/lib/server/storage/s3.ts:183-190 defaults public URLs to /api/storage/${key}.
- apps/web/src/routes/api/storage/$.ts:112-178 serves or redirects for any valid key without auth.
- apps/web/src/lib/server/storage/s3.ts:496-510 uses a default presigned GET expiration of 172800 seconds, or 48 hours.
- apps/web/src/lib/server/storage/s3.ts:327-335 uses crypto.randomUUID().slice(0, 8) and includes the sanitized original filename in the key.
- apps/web/src/routes/api/storage/$.ts:6-9 creates an unbounded proxyCache.
- apps/web/src/routes/api/storage/$.ts:150-153 buffers the full S3 object and stores it in the cache.
Impact
Any leaked, logged, guessed, or shared object key becomes enough to access the file. Private chat attachments, admin uploads, imported assets, and internal objects cannot be meaningfully protected if they share this route. The unbounded proxy cache also creates a memory pressure vector when proxy mode or ?email=1 is used.
Recommended fix
Introduce storage object visibility and ownership. Public assets can continue to use stable public URLs. Private assets should require a short-lived, signed, purpose-scoped read token or authenticated authorization check. Increase object key entropy and stop embedding original filenames in sensitive keys. Add LRU and byte caps to the proxy cache, or stream proxy responses without caching by default.
Acceptance criteria
- Storage objects are classified as public or private at creation time.
- Private objects cannot be fetched from /api/storage/* without authorization or a signed read token.
- Presigned GET TTL is reduced for private content.
- Storage keys use high-entropy random IDs and avoid exposing original filenames for private objects.
- Proxy cache has maximum entry count and maximum byte size, with tests for eviction and oversized objects.
Summary
GET /api/storage/* serves any object whose key is known. It does not enforce object ownership, visibility, prefix policy, or signed read authorization. The default public URL builder points to this route, presigned GET URLs last 48 hours, and storage keys include only 8 UUID characters plus the original sanitized filename.
The proxy path also stores full S3 objects in an unbounded in-memory Map.
Evidence
Impact
Any leaked, logged, guessed, or shared object key becomes enough to access the file. Private chat attachments, admin uploads, imported assets, and internal objects cannot be meaningfully protected if they share this route. The unbounded proxy cache also creates a memory pressure vector when proxy mode or ?email=1 is used.
Recommended fix
Introduce storage object visibility and ownership. Public assets can continue to use stable public URLs. Private assets should require a short-lived, signed, purpose-scoped read token or authenticated authorization check. Increase object key entropy and stop embedding original filenames in sensitive keys. Add LRU and byte caps to the proxy cache, or stream proxy responses without caching by default.
Acceptance criteria