mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-14 06:28:52 +01:00
* 🔧 fix: MCP server ID resolver in access permissions (#11315) - Replaced `findMCPServerById` with `findMCPServerByObjectId` in access permissions route and corresponding tests for improved clarity and consistency in resource identification. * 🔧 refactor: Update MCP server resource access methods to use server name - Replaced instances of `findMCPServerById` with `findMCPServerByServerName` across middleware, database, and test files for improved clarity and consistency in resource identification. - Updated related comments and test cases to reflect the change in method usage. * chore: Increase timeout for Redis update in GenerationJobManager integration tests - Updated the timeout duration from 50ms to 200ms in the GenerationJobManager integration tests to ensure reliable verification of final event data in Redis after emitting the done event.
228 lines
7.3 KiB
JavaScript
228 lines
7.3 KiB
JavaScript
const express = require('express');
|
|
const request = require('supertest');
|
|
const mongoose = require('mongoose');
|
|
const { v4: uuidv4 } = require('uuid');
|
|
const { createMethods } = require('@librechat/data-schemas');
|
|
const { MongoMemoryServer } = require('mongodb-memory-server');
|
|
const { ResourceType, PermissionBits } = require('librechat-data-provider');
|
|
const { createAgent } = require('~/models/Agent');
|
|
|
|
/**
|
|
* Mock the PermissionsController to isolate route testing
|
|
*/
|
|
jest.mock('~/server/controllers/PermissionsController', () => ({
|
|
getUserEffectivePermissions: jest.fn((req, res) => res.json({ permissions: [] })),
|
|
getAllEffectivePermissions: jest.fn((req, res) => res.json({ permissions: [] })),
|
|
updateResourcePermissions: jest.fn((req, res) => res.json({ success: true })),
|
|
getResourcePermissions: jest.fn((req, res) =>
|
|
res.json({
|
|
resourceType: req.params.resourceType,
|
|
resourceId: req.params.resourceId,
|
|
principals: [],
|
|
public: false,
|
|
}),
|
|
),
|
|
getResourceRoles: jest.fn((req, res) => res.json({ roles: [] })),
|
|
searchPrincipals: jest.fn((req, res) => res.json({ principals: [] })),
|
|
}));
|
|
|
|
jest.mock('~/server/middleware/checkPeoplePickerAccess', () => ({
|
|
checkPeoplePickerAccess: jest.fn((req, res, next) => next()),
|
|
}));
|
|
|
|
// Import actual middleware to get canAccessResource
|
|
const { canAccessResource } = require('~/server/middleware');
|
|
const { findMCPServerByObjectId } = require('~/models');
|
|
|
|
/**
|
|
* Security Tests for SBA-ADV-20251203-02
|
|
*
|
|
* These tests verify that users cannot query or modify agent permissions
|
|
* without proper SHARE permission.
|
|
*/
|
|
describe('Access Permissions Routes - Security Tests (SBA-ADV-20251203-02)', () => {
|
|
let app;
|
|
let mongoServer;
|
|
let authorId;
|
|
let attackerId;
|
|
let agentId;
|
|
let methods;
|
|
let User;
|
|
let modelsToCleanup = [];
|
|
|
|
beforeAll(async () => {
|
|
mongoServer = await MongoMemoryServer.create();
|
|
const mongoUri = mongoServer.getUri();
|
|
await mongoose.connect(mongoUri);
|
|
|
|
// Initialize models
|
|
const { createModels } = require('@librechat/data-schemas');
|
|
const models = createModels(mongoose);
|
|
modelsToCleanup = Object.keys(models);
|
|
Object.assign(mongoose.models, models);
|
|
|
|
methods = createMethods(mongoose);
|
|
User = models.User;
|
|
|
|
await methods.seedDefaultRoles();
|
|
});
|
|
|
|
afterAll(async () => {
|
|
const collections = mongoose.connection.collections;
|
|
for (const key in collections) {
|
|
await collections[key].deleteMany({});
|
|
}
|
|
for (const modelName of modelsToCleanup) {
|
|
delete mongoose.models[modelName];
|
|
}
|
|
await mongoose.disconnect();
|
|
await mongoServer.stop();
|
|
});
|
|
|
|
beforeEach(async () => {
|
|
// Clear all collections
|
|
const collections = mongoose.connection.collections;
|
|
for (const key in collections) {
|
|
await collections[key].deleteMany({});
|
|
}
|
|
await methods.seedDefaultRoles();
|
|
|
|
// Create author (owner of the agent)
|
|
authorId = new mongoose.Types.ObjectId().toString();
|
|
await User.create({
|
|
_id: authorId,
|
|
name: 'Agent Owner',
|
|
email: 'owner@example.com',
|
|
username: 'owner@example.com',
|
|
provider: 'local',
|
|
});
|
|
|
|
// Create attacker (should not have access)
|
|
attackerId = new mongoose.Types.ObjectId().toString();
|
|
await User.create({
|
|
_id: attackerId,
|
|
name: 'Attacker',
|
|
email: 'attacker@example.com',
|
|
username: 'attacker@example.com',
|
|
provider: 'local',
|
|
});
|
|
|
|
// Create private agent owned by author
|
|
const customAgentId = `agent_${uuidv4().replace(/-/g, '').substring(0, 20)}`;
|
|
await createAgent({
|
|
id: customAgentId,
|
|
name: 'Private Agent',
|
|
provider: 'openai',
|
|
model: 'gpt-4',
|
|
author: authorId,
|
|
});
|
|
agentId = customAgentId;
|
|
|
|
// Create Express app with attacker as current user
|
|
app = express();
|
|
app.use(express.json());
|
|
|
|
// Mock authentication middleware - attacker is the current user
|
|
app.use((req, res, next) => {
|
|
req.user = { id: attackerId, role: 'USER' };
|
|
req.app = { locals: {} };
|
|
next();
|
|
});
|
|
|
|
// Middleware factory for permission access check (mirrors actual implementation)
|
|
const checkResourcePermissionAccess = (requiredPermission) => (req, res, next) => {
|
|
const { resourceType } = req.params;
|
|
let middleware;
|
|
|
|
if (resourceType === ResourceType.AGENT) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.AGENT,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
});
|
|
} else if (resourceType === ResourceType.PROMPTGROUP) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.PROMPTGROUP,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
});
|
|
} else if (resourceType === ResourceType.MCPSERVER) {
|
|
middleware = canAccessResource({
|
|
resourceType: ResourceType.MCPSERVER,
|
|
requiredPermission,
|
|
resourceIdParam: 'resourceId',
|
|
idResolver: findMCPServerByObjectId,
|
|
});
|
|
} else {
|
|
return res.status(400).json({
|
|
error: 'Bad Request',
|
|
message: `Unsupported resource type: ${resourceType}`,
|
|
});
|
|
}
|
|
|
|
middleware(req, res, next);
|
|
};
|
|
|
|
// GET route with access control (THE FIX)
|
|
app.get(
|
|
'/permissions/:resourceType/:resourceId',
|
|
checkResourcePermissionAccess(PermissionBits.SHARE),
|
|
(req, res) =>
|
|
res.json({
|
|
resourceType: req.params.resourceType,
|
|
resourceId: req.params.resourceId,
|
|
principals: [],
|
|
public: false,
|
|
}),
|
|
);
|
|
|
|
// PUT route with access control
|
|
app.put(
|
|
'/permissions/:resourceType/:resourceId',
|
|
checkResourcePermissionAccess(PermissionBits.SHARE),
|
|
(req, res) => res.json({ success: true }),
|
|
);
|
|
});
|
|
|
|
describe('GET /permissions/:resourceType/:resourceId', () => {
|
|
it('should deny permission query for user without access (main vulnerability test)', async () => {
|
|
/**
|
|
* SECURITY TEST: This is the core test for SBA-ADV-20251203-02
|
|
*
|
|
* Before the fix, any authenticated user could query permissions for
|
|
* any agent by just knowing the agent ID, exposing information about
|
|
* who has access to private agents.
|
|
*
|
|
* After the fix, users must have SHARE permission to view permissions.
|
|
*/
|
|
const response = await request(app)
|
|
.get(`/permissions/agent/${agentId}`)
|
|
.set('Content-Type', 'application/json');
|
|
|
|
// Should be denied - attacker has no permission on the agent
|
|
expect(response.status).toBe(403);
|
|
expect(response.body.error).toBe('Forbidden');
|
|
});
|
|
|
|
it('should return 400 for unsupported resource type', async () => {
|
|
const response = await request(app)
|
|
.get(`/permissions/unsupported/${agentId}`)
|
|
.set('Content-Type', 'application/json');
|
|
|
|
expect(response.status).toBe(400);
|
|
expect(response.body.message).toContain('Unsupported resource type');
|
|
});
|
|
});
|
|
|
|
describe('PUT /permissions/:resourceType/:resourceId', () => {
|
|
it('should deny permission update for user without access', async () => {
|
|
const response = await request(app)
|
|
.put(`/permissions/agent/${agentId}`)
|
|
.set('Content-Type', 'application/json')
|
|
.send({ principals: [] });
|
|
|
|
expect(response.status).toBe(403);
|
|
expect(response.body.error).toBe('Forbidden');
|
|
});
|
|
});
|
|
});
|