mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-06 10:38:50 +01:00
229 lines
7.3 KiB
JavaScript
229 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 { findMCPServerById } = 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: findMCPServerById,
|
||
|
|
});
|
||
|
|
} 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');
|
||
|
|
});
|
||
|
|
});
|
||
|
|
});
|