mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-17 08:50:15 +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
5
api/cache/getLogStores.js
vendored
5
api/cache/getLogStores.js
vendored
|
|
@ -35,11 +35,11 @@ const messages = isEnabled(USE_REDIS)
|
||||||
? new Keyv({ store: keyvRedis, ttl: Time.FIVE_MINUTES })
|
? new Keyv({ store: keyvRedis, ttl: Time.FIVE_MINUTES })
|
||||||
: new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.FIVE_MINUTES });
|
: new Keyv({ namespace: CacheKeys.MESSAGES, ttl: Time.FIVE_MINUTES });
|
||||||
|
|
||||||
const tokenConfig = isEnabled(USE_REDIS) // ttl: 30 minutes
|
const tokenConfig = isEnabled(USE_REDIS)
|
||||||
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
|
? new Keyv({ store: keyvRedis, ttl: Time.THIRTY_MINUTES })
|
||||||
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES });
|
: new Keyv({ namespace: CacheKeys.TOKEN_CONFIG, ttl: Time.THIRTY_MINUTES });
|
||||||
|
|
||||||
const genTitle = isEnabled(USE_REDIS) // ttl: 2 minutes
|
const genTitle = isEnabled(USE_REDIS)
|
||||||
? new Keyv({ store: keyvRedis, ttl: Time.TWO_MINUTES })
|
? new Keyv({ store: keyvRedis, ttl: Time.TWO_MINUTES })
|
||||||
: new Keyv({ namespace: CacheKeys.GEN_TITLE, ttl: Time.TWO_MINUTES });
|
: new Keyv({ namespace: CacheKeys.GEN_TITLE, ttl: Time.TWO_MINUTES });
|
||||||
|
|
||||||
|
|
@ -69,6 +69,7 @@ const namespaces = {
|
||||||
registrations: createViolationInstance('registrations'),
|
registrations: createViolationInstance('registrations'),
|
||||||
[ViolationTypes.TTS_LIMIT]: createViolationInstance(ViolationTypes.TTS_LIMIT),
|
[ViolationTypes.TTS_LIMIT]: createViolationInstance(ViolationTypes.TTS_LIMIT),
|
||||||
[ViolationTypes.STT_LIMIT]: createViolationInstance(ViolationTypes.STT_LIMIT),
|
[ViolationTypes.STT_LIMIT]: createViolationInstance(ViolationTypes.STT_LIMIT),
|
||||||
|
[ViolationTypes.CONVO_ACCESS]: createViolationInstance(ViolationTypes.CONVO_ACCESS),
|
||||||
[ViolationTypes.FILE_UPLOAD_LIMIT]: createViolationInstance(ViolationTypes.FILE_UPLOAD_LIMIT),
|
[ViolationTypes.FILE_UPLOAD_LIMIT]: createViolationInstance(ViolationTypes.FILE_UPLOAD_LIMIT),
|
||||||
[ViolationTypes.VERIFY_EMAIL_LIMIT]: createViolationInstance(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
[ViolationTypes.VERIFY_EMAIL_LIMIT]: createViolationInstance(ViolationTypes.VERIFY_EMAIL_LIMIT),
|
||||||
[ViolationTypes.RESET_PASSWORD_LIMIT]: createViolationInstance(
|
[ViolationTypes.RESET_PASSWORD_LIMIT]: createViolationInstance(
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
const clearPendingReq = require('../../cache/clearPendingReq');
|
const { Time } = require('librechat-data-provider');
|
||||||
const { logViolation, getLogStores } = require('../../cache');
|
const clearPendingReq = require('~/cache/clearPendingReq');
|
||||||
|
const { logViolation, getLogStores } = require('~/cache');
|
||||||
|
const { isEnabled } = require('~/server/utils');
|
||||||
const denyRequest = require('./denyRequest');
|
const denyRequest = require('./denyRequest');
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|
@ -7,7 +9,6 @@ const {
|
||||||
CONCURRENT_MESSAGE_MAX = 1,
|
CONCURRENT_MESSAGE_MAX = 1,
|
||||||
CONCURRENT_VIOLATION_SCORE: score,
|
CONCURRENT_VIOLATION_SCORE: score,
|
||||||
} = process.env ?? {};
|
} = process.env ?? {};
|
||||||
const ttl = 1000 * 60 * 1;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Middleware to limit concurrent requests for a user.
|
* 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 limit = Math.max(CONCURRENT_MESSAGE_MAX, 1);
|
||||||
const type = 'concurrent';
|
const type = 'concurrent';
|
||||||
|
|
||||||
const key = `${USE_REDIS ? namespace : ''}:${userId}`;
|
const key = `${isEnabled(USE_REDIS) ? namespace : ''}:${userId}`;
|
||||||
const pendingRequests = +((await cache.get(key)) ?? 0);
|
const pendingRequests = +((await cache.get(key)) ?? 0);
|
||||||
|
|
||||||
if (pendingRequests >= limit) {
|
if (pendingRequests >= limit) {
|
||||||
|
|
@ -51,7 +52,7 @@ const concurrentLimiter = async (req, res, next) => {
|
||||||
await logViolation(req, res, type, errorMessage, score);
|
await logViolation(req, res, type, errorMessage, score);
|
||||||
return await denyRequest(req, res, errorMessage);
|
return await denyRequest(req, res, errorMessage);
|
||||||
} else {
|
} 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
|
// 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 validateModel = require('./validateModel');
|
||||||
const moderateText = require('./moderateText');
|
const moderateText = require('./moderateText');
|
||||||
const setHeaders = require('./setHeaders');
|
const setHeaders = require('./setHeaders');
|
||||||
|
const validate = require('./validate');
|
||||||
const limiters = require('./limiters');
|
const limiters = require('./limiters');
|
||||||
const uaParser = require('./uaParser');
|
const uaParser = require('./uaParser');
|
||||||
const checkBan = require('./checkBan');
|
const checkBan = require('./checkBan');
|
||||||
|
|
@ -22,6 +23,7 @@ const roles = require('./roles');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
...abortMiddleware,
|
...abortMiddleware,
|
||||||
|
...validate,
|
||||||
...limiters,
|
...limiters,
|
||||||
...roles,
|
...roles,
|
||||||
noIndex,
|
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,
|
||||||
|
};
|
||||||
|
|
@ -12,9 +12,10 @@ const {
|
||||||
uaParser,
|
uaParser,
|
||||||
checkBan,
|
checkBan,
|
||||||
requireJwtAuth,
|
requireJwtAuth,
|
||||||
concurrentLimiter,
|
|
||||||
messageIpLimiter,
|
messageIpLimiter,
|
||||||
|
concurrentLimiter,
|
||||||
messageUserLimiter,
|
messageUserLimiter,
|
||||||
|
validateConvoAccess,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
|
|
||||||
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
|
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
|
||||||
|
|
@ -37,6 +38,8 @@ if (isEnabled(LIMIT_MESSAGE_USER)) {
|
||||||
router.use(messageUserLimiter);
|
router.use(messageUserLimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.use(validateConvoAccess);
|
||||||
|
|
||||||
router.use([`/${EModelEndpoint.azureOpenAI}`, `/${EModelEndpoint.openAI}`], openAI);
|
router.use([`/${EModelEndpoint.azureOpenAI}`, `/${EModelEndpoint.openAI}`], openAI);
|
||||||
router.use(`/${EModelEndpoint.chatGPTBrowser}`, askChatGPTBrowser);
|
router.use(`/${EModelEndpoint.chatGPTBrowser}`, askChatGPTBrowser);
|
||||||
router.use(`/${EModelEndpoint.gptPlugins}`, gptPlugins);
|
router.use(`/${EModelEndpoint.gptPlugins}`, gptPlugins);
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const {
|
||||||
// validateEndpoint,
|
// validateEndpoint,
|
||||||
buildEndpointOption,
|
buildEndpointOption,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
|
const validateConvoAccess = require('~/server/middleware/validate/convoAccess');
|
||||||
const validateAssistant = require('~/server/middleware/assistants/validate');
|
const validateAssistant = require('~/server/middleware/assistants/validate');
|
||||||
const chatController = require('~/server/controllers/assistants/chatV1');
|
const chatController = require('~/server/controllers/assistants/chatV1');
|
||||||
|
|
||||||
|
|
@ -21,6 +22,14 @@ router.post('/abort', handleAbort());
|
||||||
* @param {express.Response} res - The response object, used to send back a response.
|
* @param {express.Response} res - The response object, used to send back a response.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
router.post('/', validateModel, buildEndpointOption, validateAssistant, setHeaders, chatController);
|
router.post(
|
||||||
|
'/',
|
||||||
|
validateModel,
|
||||||
|
buildEndpointOption,
|
||||||
|
validateAssistant,
|
||||||
|
validateConvoAccess,
|
||||||
|
setHeaders,
|
||||||
|
chatController,
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ const {
|
||||||
// validateEndpoint,
|
// validateEndpoint,
|
||||||
buildEndpointOption,
|
buildEndpointOption,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
|
const validateConvoAccess = require('~/server/middleware/validate/convoAccess');
|
||||||
const validateAssistant = require('~/server/middleware/assistants/validate');
|
const validateAssistant = require('~/server/middleware/assistants/validate');
|
||||||
const chatController = require('~/server/controllers/assistants/chatV2');
|
const chatController = require('~/server/controllers/assistants/chatV2');
|
||||||
|
|
||||||
|
|
@ -21,6 +22,14 @@ router.post('/abort', handleAbort());
|
||||||
* @param {express.Response} res - The response object, used to send back a response.
|
* @param {express.Response} res - The response object, used to send back a response.
|
||||||
* @returns {void}
|
* @returns {void}
|
||||||
*/
|
*/
|
||||||
router.post('/', validateModel, buildEndpointOption, validateAssistant, setHeaders, chatController);
|
router.post(
|
||||||
|
'/',
|
||||||
|
validateModel,
|
||||||
|
buildEndpointOption,
|
||||||
|
validateAssistant,
|
||||||
|
validateConvoAccess,
|
||||||
|
setHeaders,
|
||||||
|
chatController,
|
||||||
|
);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,6 @@
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
const {
|
const { uaParser, checkBan, requireJwtAuth } = require('~/server/middleware');
|
||||||
uaParser,
|
|
||||||
checkBan,
|
|
||||||
requireJwtAuth,
|
|
||||||
// concurrentLimiter,
|
|
||||||
// messageIpLimiter,
|
|
||||||
// messageUserLimiter,
|
|
||||||
} = require('~/server/middleware');
|
|
||||||
|
|
||||||
const v1 = require('./v1');
|
const v1 = require('./v1');
|
||||||
const chatV1 = require('./chatV1');
|
const chatV1 = require('./chatV1');
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ const {
|
||||||
messageIpLimiter,
|
messageIpLimiter,
|
||||||
concurrentLimiter,
|
concurrentLimiter,
|
||||||
messageUserLimiter,
|
messageUserLimiter,
|
||||||
|
validateConvoAccess,
|
||||||
} = require('~/server/middleware');
|
} = require('~/server/middleware');
|
||||||
|
|
||||||
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
|
const { LIMIT_CONCURRENT_MESSAGES, LIMIT_MESSAGE_IP, LIMIT_MESSAGE_USER } = process.env ?? {};
|
||||||
|
|
@ -35,6 +36,8 @@ if (isEnabled(LIMIT_MESSAGE_USER)) {
|
||||||
router.use(messageUserLimiter);
|
router.use(messageUserLimiter);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
router.use(validateConvoAccess);
|
||||||
|
|
||||||
router.use([`/${EModelEndpoint.azureOpenAI}`, `/${EModelEndpoint.openAI}`], openAI);
|
router.use([`/${EModelEndpoint.azureOpenAI}`, `/${EModelEndpoint.openAI}`], openAI);
|
||||||
router.use(`/${EModelEndpoint.gptPlugins}`, gptPlugins);
|
router.use(`/${EModelEndpoint.gptPlugins}`, gptPlugins);
|
||||||
router.use(`/${EModelEndpoint.anthropic}`, anthropic);
|
router.use(`/${EModelEndpoint.anthropic}`, anthropic);
|
||||||
|
|
|
||||||
|
|
@ -679,6 +679,7 @@ export enum InfiniteCollections {
|
||||||
* Enum for time intervals
|
* Enum for time intervals
|
||||||
*/
|
*/
|
||||||
export enum Time {
|
export enum Time {
|
||||||
|
ONE_HOUR = 3600000,
|
||||||
THIRTY_MINUTES = 1800000,
|
THIRTY_MINUTES = 1800000,
|
||||||
TEN_MINUTES = 600000,
|
TEN_MINUTES = 600000,
|
||||||
FIVE_MINUTES = 300000,
|
FIVE_MINUTES = 300000,
|
||||||
|
|
@ -799,6 +800,10 @@ export enum ViolationTypes {
|
||||||
* Verify Email Limit Violation.
|
* Verify Email Limit Violation.
|
||||||
*/
|
*/
|
||||||
VERIFY_EMAIL_LIMIT = 'verify_email_limit',
|
VERIFY_EMAIL_LIMIT = 'verify_email_limit',
|
||||||
|
/**
|
||||||
|
* Verify Conversation Access violation.
|
||||||
|
*/
|
||||||
|
CONVO_ACCESS = 'convo_access',
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue