LibreChat/api/server/routes/accessPermissions.test.js

229 lines
7.3 KiB
JavaScript
Raw Normal View History

🔒 fix: Access Control on Agent Permission Queries (#11145) Adds access control check to GET /api/permissions/:resourceType/:resourceId endpoint to prevent unauthorized disclosure of agent permission information. ## Vulnerability Summary LibreChat version 0.8.1-rc2 did not enforce proper access control when querying agent permissions. Any authenticated user could read the permissions of arbitrary agents by knowing the agent ID, even for private agents they had no access to. **Impact:** - Attackers could enumerate which users have access to private agents - Permission levels (owner, editor, viewer) were exposed - User emails and names of permitted users were disclosed - Agent's public/private sharing status was revealed **Attack Vector:** ``` GET /api/permissions/agent/{agent_id} Authorization: Bearer <any_valid_token> ``` The MongoDB ObjectId format (timestamp + process ID + counter) made it feasible to brute-force discover valid agent IDs. ## Fix Added `checkResourcePermissionAccess` middleware factory that enforces SHARE permission before allowing access to permission queries. This middleware is now applied to the GET endpoint, matching the existing access control on the PUT endpoint. **Before:** ```javascript router.get('/:resourceType/:resourceId', getResourcePermissions); ``` **After:** ```javascript router.get( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), getResourcePermissions, ); ``` The middleware handles all supported resource types: - Agent (ResourceType.AGENT) - Prompt Group (ResourceType.PROMPTGROUP) - MCP Server (ResourceType.MCPSERVER) ## Code Changes **api/server/routes/accessPermissions.js:** - Added `checkResourcePermissionAccess()` middleware factory - Applied middleware to GET /:resourceType/:resourceId endpoint - Refactored PUT endpoint to use the same middleware factory (DRY) **api/server/routes/accessPermissions.test.js:** - Added security tests verifying unauthorized access is denied - Tests confirm 403 Forbidden for users without SHARE permission ## Security Tests ``` ✓ should deny permission query for user without access (main vulnerability test) ✓ should return 400 for unsupported resource type ✓ should deny permission update for user without access
2025-12-29 15:10:31 -05:00
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');
📦 refactor: Consolidate DB models, encapsulating Mongoose usage in `data-schemas` (#11830) * chore: move database model methods to /packages/data-schemas * chore: add TypeScript ESLint rule to warn on unused variables * refactor: model imports to streamline access - Consolidated model imports across various files to improve code organization and reduce redundancy. - Updated imports for models such as Assistant, Message, Conversation, and others to a unified import path. - Adjusted middleware and service files to reflect the new import structure, ensuring functionality remains intact. - Enhanced test files to align with the new import paths, maintaining test coverage and integrity. * chore: migrate database models to packages/data-schemas and refactor all direct Mongoose Model usage outside of data-schemas * test: update agent model mocks in unit tests - Added `getAgent` mock to `client.test.js` to enhance test coverage for agent-related functionality. - Removed redundant `getAgent` and `getAgents` mocks from `openai.spec.js` and `responses.unit.spec.js` to streamline test setup and reduce duplication. - Ensured consistency in agent mock implementations across test files. * fix: update types in data-schemas * refactor: enhance type definitions in transaction and spending methods - Updated type definitions in `checkBalance.ts` to use specific request and response types. - Refined `spendTokens.ts` to utilize a new `SpendTxData` interface for better clarity and type safety. - Improved transaction handling in `transaction.ts` by introducing `TransactionResult` and `TxData` interfaces, ensuring consistent data structures across methods. - Adjusted unit tests in `transaction.spec.ts` to accommodate new type definitions and enhance robustness. * refactor: streamline model imports and enhance code organization - Consolidated model imports across various controllers and services to a unified import path, improving code clarity and reducing redundancy. - Updated multiple files to reflect the new import structure, ensuring all functionalities remain intact. - Enhanced overall code organization by removing duplicate import statements and optimizing the usage of model methods. * feat: implement loadAddedAgent and refactor agent loading logic - Introduced `loadAddedAgent` function to handle loading agents from added conversations, supporting multi-convo parallel execution. - Created a new `load.ts` file to encapsulate agent loading functionalities, including `loadEphemeralAgent` and `loadAgent`. - Updated the `index.ts` file to export the new `load` module instead of the deprecated `loadAgent`. - Enhanced type definitions and improved error handling in the agent loading process. - Adjusted unit tests to reflect changes in the agent loading structure and ensure comprehensive coverage. * refactor: enhance balance handling with new update interface - Introduced `IBalanceUpdate` interface to streamline balance update operations across the codebase. - Updated `upsertBalanceFields` method signatures in `balance.ts`, `transaction.ts`, and related tests to utilize the new interface for improved type safety. - Adjusted type imports in `balance.spec.ts` to include `IBalanceUpdate`, ensuring consistency in balance management functionalities. - Enhanced overall code clarity and maintainability by refining type definitions related to balance operations. * feat: add unit tests for loadAgent functionality and enhance agent loading logic - Introduced comprehensive unit tests for the `loadAgent` function, covering various scenarios including null and empty agent IDs, loading of ephemeral agents, and permission checks. - Enhanced the `initializeClient` function by moving `getConvoFiles` to the correct position in the database method exports, ensuring proper functionality. - Improved test coverage for agent loading, including handling of non-existent agents and user permissions. * chore: reorder memory method exports for consistency - Moved `deleteAllUserMemories` to the correct position in the exported memory methods, ensuring a consistent and logical order of method exports in `memory.ts`.
2026-02-17 18:23:44 -05:00
const { createAgent } = require('~/models');
🔒 fix: Access Control on Agent Permission Queries (#11145) Adds access control check to GET /api/permissions/:resourceType/:resourceId endpoint to prevent unauthorized disclosure of agent permission information. ## Vulnerability Summary LibreChat version 0.8.1-rc2 did not enforce proper access control when querying agent permissions. Any authenticated user could read the permissions of arbitrary agents by knowing the agent ID, even for private agents they had no access to. **Impact:** - Attackers could enumerate which users have access to private agents - Permission levels (owner, editor, viewer) were exposed - User emails and names of permitted users were disclosed - Agent's public/private sharing status was revealed **Attack Vector:** ``` GET /api/permissions/agent/{agent_id} Authorization: Bearer <any_valid_token> ``` The MongoDB ObjectId format (timestamp + process ID + counter) made it feasible to brute-force discover valid agent IDs. ## Fix Added `checkResourcePermissionAccess` middleware factory that enforces SHARE permission before allowing access to permission queries. This middleware is now applied to the GET endpoint, matching the existing access control on the PUT endpoint. **Before:** ```javascript router.get('/:resourceType/:resourceId', getResourcePermissions); ``` **After:** ```javascript router.get( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), getResourcePermissions, ); ``` The middleware handles all supported resource types: - Agent (ResourceType.AGENT) - Prompt Group (ResourceType.PROMPTGROUP) - MCP Server (ResourceType.MCPSERVER) ## Code Changes **api/server/routes/accessPermissions.js:** - Added `checkResourcePermissionAccess()` middleware factory - Applied middleware to GET /:resourceType/:resourceId endpoint - Refactored PUT endpoint to use the same middleware factory (DRY) **api/server/routes/accessPermissions.test.js:** - Added security tests verifying unauthorized access is denied - Tests confirm 403 Forbidden for users without SHARE permission ## Security Tests ``` ✓ should deny permission query for user without access (main vulnerability test) ✓ should return 400 for unsupported resource type ✓ should deny permission update for user without access
2025-12-29 15:10:31 -05:00
/**
* 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');
🔒 fix: Access Control on Agent Permission Queries (#11145) Adds access control check to GET /api/permissions/:resourceType/:resourceId endpoint to prevent unauthorized disclosure of agent permission information. ## Vulnerability Summary LibreChat version 0.8.1-rc2 did not enforce proper access control when querying agent permissions. Any authenticated user could read the permissions of arbitrary agents by knowing the agent ID, even for private agents they had no access to. **Impact:** - Attackers could enumerate which users have access to private agents - Permission levels (owner, editor, viewer) were exposed - User emails and names of permitted users were disclosed - Agent's public/private sharing status was revealed **Attack Vector:** ``` GET /api/permissions/agent/{agent_id} Authorization: Bearer <any_valid_token> ``` The MongoDB ObjectId format (timestamp + process ID + counter) made it feasible to brute-force discover valid agent IDs. ## Fix Added `checkResourcePermissionAccess` middleware factory that enforces SHARE permission before allowing access to permission queries. This middleware is now applied to the GET endpoint, matching the existing access control on the PUT endpoint. **Before:** ```javascript router.get('/:resourceType/:resourceId', getResourcePermissions); ``` **After:** ```javascript router.get( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), getResourcePermissions, ); ``` The middleware handles all supported resource types: - Agent (ResourceType.AGENT) - Prompt Group (ResourceType.PROMPTGROUP) - MCP Server (ResourceType.MCPSERVER) ## Code Changes **api/server/routes/accessPermissions.js:** - Added `checkResourcePermissionAccess()` middleware factory - Applied middleware to GET /:resourceType/:resourceId endpoint - Refactored PUT endpoint to use the same middleware factory (DRY) **api/server/routes/accessPermissions.test.js:** - Added security tests verifying unauthorized access is denied - Tests confirm 403 Forbidden for users without SHARE permission ## Security Tests ``` ✓ should deny permission query for user without access (main vulnerability test) ✓ should return 400 for unsupported resource type ✓ should deny permission update for user without access
2025-12-29 15:10:31 -05:00
/**
* 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,
🔒 fix: Access Control on Agent Permission Queries (#11145) Adds access control check to GET /api/permissions/:resourceType/:resourceId endpoint to prevent unauthorized disclosure of agent permission information. ## Vulnerability Summary LibreChat version 0.8.1-rc2 did not enforce proper access control when querying agent permissions. Any authenticated user could read the permissions of arbitrary agents by knowing the agent ID, even for private agents they had no access to. **Impact:** - Attackers could enumerate which users have access to private agents - Permission levels (owner, editor, viewer) were exposed - User emails and names of permitted users were disclosed - Agent's public/private sharing status was revealed **Attack Vector:** ``` GET /api/permissions/agent/{agent_id} Authorization: Bearer <any_valid_token> ``` The MongoDB ObjectId format (timestamp + process ID + counter) made it feasible to brute-force discover valid agent IDs. ## Fix Added `checkResourcePermissionAccess` middleware factory that enforces SHARE permission before allowing access to permission queries. This middleware is now applied to the GET endpoint, matching the existing access control on the PUT endpoint. **Before:** ```javascript router.get('/:resourceType/:resourceId', getResourcePermissions); ``` **After:** ```javascript router.get( '/:resourceType/:resourceId', checkResourcePermissionAccess(PermissionBits.SHARE), getResourcePermissions, ); ``` The middleware handles all supported resource types: - Agent (ResourceType.AGENT) - Prompt Group (ResourceType.PROMPTGROUP) - MCP Server (ResourceType.MCPSERVER) ## Code Changes **api/server/routes/accessPermissions.js:** - Added `checkResourcePermissionAccess()` middleware factory - Applied middleware to GET /:resourceType/:resourceId endpoint - Refactored PUT endpoint to use the same middleware factory (DRY) **api/server/routes/accessPermissions.test.js:** - Added security tests verifying unauthorized access is denied - Tests confirm 403 Forbidden for users without SHARE permission ## Security Tests ``` ✓ should deny permission query for user without access (main vulnerability test) ✓ should return 400 for unsupported resource type ✓ should deny permission update for user without access
2025-12-29 15:10:31 -05:00
});
} 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');
});
});
});