| const cookies = require('cookie'); |
| const jwt = require('jsonwebtoken'); |
| const { logger } = require('@librechat/data-schemas'); |
| const { isEnabled, getBasePath } = require('@librechat/api'); |
|
|
| const OBJECT_ID_LENGTH = 24; |
| const OBJECT_ID_PATTERN = /^[0-9a-f]{24}$/i; |
|
|
| |
| |
| |
| |
| |
| function isValidObjectId(id) { |
| if (typeof id !== 'string') { |
| return false; |
| } |
| if (id.length !== OBJECT_ID_LENGTH) { |
| return false; |
| } |
| return OBJECT_ID_PATTERN.test(id); |
| } |
|
|
| |
| |
| |
| |
| |
| function validateToken(refreshToken) { |
| try { |
| const payload = jwt.verify(refreshToken, process.env.JWT_REFRESH_SECRET); |
|
|
| if (!isValidObjectId(payload.id)) { |
| return { valid: false, error: 'Invalid User ID' }; |
| } |
|
|
| const currentTimeInSeconds = Math.floor(Date.now() / 1000); |
| if (payload.exp < currentTimeInSeconds) { |
| return { valid: false, error: 'Refresh token expired' }; |
| } |
|
|
| return { valid: true, userId: payload.id }; |
| } catch (err) { |
| logger.warn('[validateToken]', err); |
| return { valid: false, error: 'Invalid token' }; |
| } |
| } |
|
|
| |
| |
| |
| |
| function createValidateImageRequest(secureImageLinks) { |
| if (!secureImageLinks) { |
| return (_req, _res, next) => next(); |
| } |
| |
| |
| |
| |
| |
| return async function validateImageRequest(req, res, next) { |
| try { |
| const cookieHeader = req.headers.cookie; |
| if (!cookieHeader) { |
| logger.warn('[validateImageRequest] No cookies provided'); |
| return res.status(401).send('Unauthorized'); |
| } |
|
|
| const parsedCookies = cookies.parse(cookieHeader); |
| const refreshToken = parsedCookies.refreshToken; |
|
|
| if (!refreshToken) { |
| logger.warn('[validateImageRequest] Token not provided'); |
| return res.status(401).send('Unauthorized'); |
| } |
|
|
| const tokenProvider = parsedCookies.token_provider; |
| let userIdForPath; |
|
|
| if (tokenProvider === 'openid' && isEnabled(process.env.OPENID_REUSE_TOKENS)) { |
| const openidUserId = parsedCookies.openid_user_id; |
| if (!openidUserId) { |
| logger.warn('[validateImageRequest] No OpenID user ID cookie found'); |
| return res.status(403).send('Access Denied'); |
| } |
|
|
| const validationResult = validateToken(openidUserId); |
| if (!validationResult.valid) { |
| logger.warn(`[validateImageRequest] ${validationResult.error}`); |
| return res.status(403).send('Access Denied'); |
| } |
| userIdForPath = validationResult.userId; |
| } else { |
| const validationResult = validateToken(refreshToken); |
| if (!validationResult.valid) { |
| logger.warn(`[validateImageRequest] ${validationResult.error}`); |
| return res.status(403).send('Access Denied'); |
| } |
| userIdForPath = validationResult.userId; |
| } |
|
|
| if (!userIdForPath) { |
| logger.warn('[validateImageRequest] No user ID available for path validation'); |
| return res.status(403).send('Access Denied'); |
| } |
|
|
| const MAX_URL_LENGTH = 2048; |
| if (req.originalUrl.length > MAX_URL_LENGTH) { |
| logger.warn('[validateImageRequest] URL too long'); |
| return res.status(403).send('Access Denied'); |
| } |
|
|
| if (req.originalUrl.includes('\x00')) { |
| logger.warn('[validateImageRequest] URL contains null byte'); |
| return res.status(403).send('Access Denied'); |
| } |
|
|
| let fullPath; |
| try { |
| fullPath = decodeURIComponent(req.originalUrl); |
| } catch { |
| logger.warn('[validateImageRequest] Invalid URL encoding'); |
| return res.status(403).send('Access Denied'); |
| } |
|
|
| const basePath = getBasePath(); |
| const imagesPath = `${basePath}/images`; |
|
|
| const agentAvatarPattern = new RegExp( |
| `^${imagesPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/[a-f0-9]{24}/agent-[^/]*$`, |
| ); |
| if (agentAvatarPattern.test(fullPath)) { |
| logger.debug('[validateImageRequest] Image request validated'); |
| return next(); |
| } |
|
|
| const escapedUserId = userIdForPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); |
| const pathPattern = new RegExp( |
| `^${imagesPath.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}/${escapedUserId}/[^/]+$`, |
| ); |
|
|
| if (pathPattern.test(fullPath)) { |
| logger.debug('[validateImageRequest] Image request validated'); |
| next(); |
| } else { |
| logger.warn('[validateImageRequest] Invalid image path'); |
| res.status(403).send('Access Denied'); |
| } |
| } catch (error) { |
| logger.error('[validateImageRequest] Error:', error); |
| res.status(500).send('Internal Server Error'); |
| } |
| }; |
| } |
|
|
| module.exports = createValidateImageRequest; |
|
|