Repository navigation
Conversation
The fileAccess middleware decided inline whether a user may access a file: same tenant for tenant-scoped files, then ownership, then access through the agents the file is attached to. The decision now lives in canAccessFile(user, file), which the middleware calls and exports, so a route that already holds a file record can apply the same rules without going through req.params.file_id. Behaviour is unchanged; the cross-tenant denial is still logged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
With fileStrategy s3, browsers load images, previews and downloads straight from presigned URLs. Those links expire after S3_URL_EXPIRY_SECONDS and are stored in messages and file records, so images break once they expire, and the bucket has to answer requests from the internet, so it cannot be kept private to the deployment's network (for instance with a bucket policy on aws:SourceVpce). STORAGE_PROXY_FILES=true makes storage strategies link files to LibreChat instead: /api/stored-files/<source>/<key>, which never expires. The route reads the object through the strategy's StoredFileSource adapter and streams it to viewers who may read it. This commit adds the storage-agnostic route and links, and the S3 adapter. With the proxy on, getS3URL returns stored file links, extractKeyFromS3Url reads them back to keys, presigned links are replaced where the file list and avatars already renew links, and getS3DownloadURL returns no direct link, so the download and shared-link routes stream through their own checks. With it off, stored links go back to presigned ones the same way. Off by default. The route signs the viewer in from the session cookie, as /images does, since an image tag sends no bearer token; authenticateViewer carries the image route's account checks (retired tokens, required 2FA enrollment). Within the viewer's tenant it serves the owner named in the key, avatars to any signed-in user (as CloudFront avatar cookies do), and anyone the stored file's own rules admit through canAccessFile, such as a file on an agent shared with the viewer; everyone else gets a 404. The file lookup is scoped to the key's owner and source. Uploads are now served from the app's own origin, so only raster images are sent inline; every response carries a sandboxing CSP and nosniff. Related to LibreChat-AI#16912 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Vidminas
marked this pull request as ready for review
October 9, 2026 10:56
7 of 15 tasks
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.
Pull Request
👍
Summary
With
fileStrategy: "s3", browsers load files from presigned URLs. Those expire afterS3_URL_EXPIRY_SECONDSand are stored in messages and file records, so images break (#10269, #10145, #13762). Because browsers fetch from S3 directly, the bucket also can't be kept private to the deployment's network.STORAGE_PROXY_FILES=truemakes storage strategies link files to LibreChat instead:/api/stored-files/<source>/<key>, which never expires. The route streams each object to viewers who may read it. This PR adds the storage-agnostic route and the S3 adapter. Off by default.Related to #16912. Depends on #16913
How it works
With the proxy on, the S3 strategy:
extractKeyFromS3Url;With it off, stored links go back to presigned ones the same way.
Responses: only raster images are sent inline, and every response carries a sandboxing CSP and
nosniff.Type of change
Testing
fileStrategy: "s3"andSTORAGE_PROXY_FILES=true, and start the server./api/stored-files/s3/..., and the browser makes no request to S3.S3_URL_EXPIRY_SECONDS(defaults to 2 minutes) and reload. The image still loads.Tested environments/configuration:
mainatcd5dd94de;fileStrategy: s3withSTORAGE_PROXY_FILES=true;Automated tests:
npm run static-checks -- --against upstream/dev --fullpasses.npm run lighthousewith Node 24.18.0, MongoDB 8.2.1 viamongodb-memory-server11.0.1, S3 mocked withaws-sdk-client-mock, and Google Chrome 154.0.8037.97 (headless). The medians matcheddev: LCP 3,432 ms against 3,455 ms, CLS 0.018 against 0.018, TBT 57 ms against 58 ms.STORAGE_PROXY_FILESon and off.New tests:
proxy/__tests__/handler.test.ts:proxy/__tests__/link.test.ts.s3/__tests__/source.test.ts: the strategy in both modes, and the adapter (GetObject, HeadObject, missing objects).images/authorization.spec.ts:authenticateViewer.routes/__tests__/storedFiles.spec.js: the real@librechat/apibuild with mocked S3.Screenshots / recordings
No user-facing change.
Risk / compatibility
Checklist