🍪 fix: Validate Shared-File Cookie Auth Against the Live Refresh Session (#13908)

* fix: validate shared file cookie sessions

* fix: run shared file session lookup as system
This commit is contained in:
Danny Avila 2026-06-23 08:32:28 -04:00 committed by GitHub
parent 725a14e409
commit ddc763595a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 25 deletions

View file

@ -3,9 +3,9 @@ const jwt = require('jsonwebtoken');
const { isEnabled } = require('@librechat/api');
const { logger, runAsSystem } = require('@librechat/data-schemas');
const { SystemRoles } = require('librechat-data-provider');
const { getUserById } = require('~/models');
const { getUserById, findSession } = require('~/models');
const verifyRefreshToken = (token) => {
const verifySignedUserId = (token) => {
try {
const payload = jwt.verify(token, process.env.JWT_REFRESH_SECRET);
return typeof payload?.id === 'string' ? payload.id : null;
@ -14,13 +14,36 @@ const verifyRefreshToken = (token) => {
}
};
const getRefreshTokenUserId = async (token) => {
const userId = verifySignedUserId(token);
if (!userId) {
return null;
}
const session = await runAsSystem(() => findSession({ userId, refreshToken: token }));
return session ? userId : null;
};
const getOpenIdUserId = (parsed, req) => {
if (parsed.token_provider !== 'openid' || !isEnabled(process.env.OPENID_REUSE_TOKENS)) {
return null;
}
const sessionRefreshToken = req.session?.openidTokens?.refreshToken;
if (!parsed.refreshToken || parsed.refreshToken !== sessionRefreshToken) {
return null;
}
return verifySignedUserId(parsed.openid_user_id);
};
/**
* Fallback auth for share file routes that are hit by `<img>`/anchor requests,
* which can't carry the bearer access token. Resolves the viewer from the
* `refreshToken` cookie (or the signed `openid_user_id` cookie) the same
* mechanism secure image links use so non-public shared links can authorize
* the viewer's ACL. Never blocks: on any failure it leaves `req.user` unset and
* lets `canAccessSharedLink` decide (public access, 401, or 403).
* `refreshToken` cookie (or an active OpenID session plus signed `openid_user_id`
* cookie) so non-public shared links can authorize the viewer's ACL. Never
* blocks: on any failure it leaves `req.user` unset and lets
* `canAccessSharedLink` decide (public access, 401, or 403).
*/
const optionalShareFileAuth = async (req, res, next) => {
if (req.user) {
@ -34,22 +57,17 @@ const optionalShareFileAuth = async (req, res, next) => {
}
const parsed = cookie.parse(cookieHeader);
const useOpenId =
parsed.token_provider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS);
const token = useOpenId ? parsed.openid_user_id : parsed.refreshToken;
if (!token) {
return next();
}
const userId = verifyRefreshToken(token);
const userId =
getOpenIdUserId(parsed, req) ||
(parsed.refreshToken ? await getRefreshTokenUserId(parsed.refreshToken) : null);
if (!userId) {
return next();
}
// Resolve in system context: this runs before canAccessSharedLink establishes
// the share tenant, so under strict tenant isolation a tenant-scoped User
// query would otherwise throw. The viewer's id comes from their own verified
// refresh token; the share's tenant-scoped ACL check still gates access.
// query would otherwise throw. The viewer's id comes from verified, active
// cookie auth; the share's tenant-scoped ACL check still gates access.
const user = await runAsSystem(() =>
getUserById(userId, '-password -__v -totpSecret -backupCodes'),
);