mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-26 13:18:51 +01:00
🛂 feat: Added Security for Conversation Access (#3588)
* 🛂 feat: Added Security for Conversation Access
* refactor: Update concurrentLimiter and convoAccess middleware to use isEnabled function for Redis check
* refactor: handle access check even if cache is not available (edge case)
This commit is contained in:
parent
b3821c1404
commit
5c99d93744
11 changed files with 121 additions and 18 deletions
|
|
@ -1,5 +1,7 @@
|
|||
const clearPendingReq = require('../../cache/clearPendingReq');
|
||||
const { logViolation, getLogStores } = require('../../cache');
|
||||
const { Time } = require('librechat-data-provider');
|
||||
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||
const { logViolation, getLogStores } = require('~/cache');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const denyRequest = require('./denyRequest');
|
||||
|
||||
const {
|
||||
|
|
@ -7,7 +9,6 @@ const {
|
|||
CONCURRENT_MESSAGE_MAX = 1,
|
||||
CONCURRENT_VIOLATION_SCORE: score,
|
||||
} = process.env ?? {};
|
||||
const ttl = 1000 * 60 * 1;
|
||||
|
||||
/**
|
||||
* Middleware to limit concurrent requests for a user.
|
||||
|
|
@ -38,7 +39,7 @@ const concurrentLimiter = async (req, res, next) => {
|
|||
const limit = Math.max(CONCURRENT_MESSAGE_MAX, 1);
|
||||
const type = 'concurrent';
|
||||
|
||||
const key = `${USE_REDIS ? namespace : ''}:${userId}`;
|
||||
const key = `${isEnabled(USE_REDIS) ? namespace : ''}:${userId}`;
|
||||
const pendingRequests = +((await cache.get(key)) ?? 0);
|
||||
|
||||
if (pendingRequests >= limit) {
|
||||
|
|
@ -51,7 +52,7 @@ const concurrentLimiter = async (req, res, next) => {
|
|||
await logViolation(req, res, type, errorMessage, score);
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
} else {
|
||||
await cache.set(key, pendingRequests + 1, ttl);
|
||||
await cache.set(key, pendingRequests + 1, Time.ONE_MINUTE);
|
||||
}
|
||||
|
||||
// Ensure the requests are removed from the store once the request is done
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ const requireJwtAuth = require('./requireJwtAuth');
|
|||
const validateModel = require('./validateModel');
|
||||
const moderateText = require('./moderateText');
|
||||
const setHeaders = require('./setHeaders');
|
||||
const validate = require('./validate');
|
||||
const limiters = require('./limiters');
|
||||
const uaParser = require('./uaParser');
|
||||
const checkBan = require('./checkBan');
|
||||
|
|
@ -22,6 +23,7 @@ const roles = require('./roles');
|
|||
|
||||
module.exports = {
|
||||
...abortMiddleware,
|
||||
...validate,
|
||||
...limiters,
|
||||
...roles,
|
||||
noIndex,
|
||||
|
|
|
|||
73
api/server/middleware/validate/convoAccess.js
Normal file
73
api/server/middleware/validate/convoAccess.js
Normal file
|
|
@ -0,0 +1,73 @@
|
|||
const { Constants, ViolationTypes, Time } = require('librechat-data-provider');
|
||||
const denyRequest = require('~/server/middleware/denyRequest');
|
||||
const { logViolation, getLogStores } = require('~/cache');
|
||||
const { isEnabled } = require('~/server/utils');
|
||||
const { getConvo } = require('~/models');
|
||||
|
||||
const { USE_REDIS, CONVO_ACCESS_VIOLATION_SCORE: score = 0 } = process.env ?? {};
|
||||
|
||||
/**
|
||||
* Middleware to validate user's authorization for a conversation.
|
||||
*
|
||||
* This middleware checks if a user has the right to access a specific conversation.
|
||||
* If the user doesn't have access, an error is returned. If the conversation doesn't exist,
|
||||
* a not found error is returned. If the access is valid, the middleware allows the request to proceed.
|
||||
* If the `cache` store is not available, the middleware will skip its logic.
|
||||
*
|
||||
* @function
|
||||
* @param {Express.Request} req - Express request object containing user information.
|
||||
* @param {Express.Response} res - Express response object.
|
||||
* @param {function} next - Express next middleware function.
|
||||
* @throws {Error} Throws an error if the user doesn't have access to the conversation.
|
||||
*/
|
||||
const validateConvoAccess = async (req, res, next) => {
|
||||
const namespace = ViolationTypes.CONVO_ACCESS;
|
||||
const cache = getLogStores(namespace);
|
||||
|
||||
const conversationId = req.body.conversationId;
|
||||
|
||||
if (!conversationId || conversationId === Constants.NEW_CONVO) {
|
||||
return next();
|
||||
}
|
||||
|
||||
const userId = req.user?.id ?? req.user?._id ?? '';
|
||||
const type = ViolationTypes.CONVO_ACCESS;
|
||||
const key = `${isEnabled(USE_REDIS) ? namespace : ''}:${userId}:${conversationId}`;
|
||||
|
||||
try {
|
||||
if (cache) {
|
||||
const cachedAccess = await cache.get(key);
|
||||
if (cachedAccess === 'authorized') {
|
||||
return next();
|
||||
}
|
||||
}
|
||||
|
||||
const conversation = await getConvo(userId, conversationId);
|
||||
|
||||
if (!conversation) {
|
||||
return next();
|
||||
}
|
||||
|
||||
if (conversation.user !== userId) {
|
||||
const errorMessage = {
|
||||
type,
|
||||
error: 'User not authorized for this conversation',
|
||||
};
|
||||
|
||||
if (cache) {
|
||||
await logViolation(req, res, type, errorMessage, score);
|
||||
}
|
||||
return await denyRequest(req, res, errorMessage);
|
||||
}
|
||||
|
||||
if (cache) {
|
||||
await cache.set(key, 'authorized', Time.TEN_MINUTES);
|
||||
}
|
||||
next();
|
||||
} catch (error) {
|
||||
console.error('Error validating conversation access:', error);
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = validateConvoAccess;
|
||||
4
api/server/middleware/validate/index.js
Normal file
4
api/server/middleware/validate/index.js
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
const validateConvoAccess = require('./convoAccess');
|
||||
module.exports = {
|
||||
validateConvoAccess,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue