🛡️ feat: Add Middleware for JSON Parsing and Prompt Group Updates (#10757)

* 🗨️ fix: Safe Validation for Prompt Updates

- Added `safeValidatePromptGroupUpdate` function to validate and sanitize prompt group update requests, ensuring only allowed fields are processed and sensitive fields are stripped.
- Updated the `patchPromptGroup` route to utilize the new validation function, returning appropriate error messages for invalid requests.
- Introduced comprehensive tests for the validation logic, covering various scenarios including allowed and disallowed fields, enhancing overall request integrity and security.
- Created a new schema file for prompt group updates, defining validation rules and types for better maintainability.

* 🔒 feat: Add JSON parse error handling middleware
This commit is contained in:
Danny Avila 2025-12-02 00:10:30 -05:00 committed by GitHub
parent 6fa94d3eb8
commit 01413eea3d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 653 additions and 1 deletions

View file

@ -16,6 +16,7 @@ const {
isEnabled,
ErrorController,
performStartupChecks,
handleJsonParseError,
initializeFileStorage,
} = require('@librechat/api');
const { connectDb, indexSync } = require('~/db');
@ -245,6 +246,7 @@ if (cluster.isMaster) {
app.use(noIndex);
app.use(express.json({ limit: '3mb' }));
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
app.use(handleJsonParseError);
app.use(mongoSanitize());
app.use(cors());
app.use(cookieParser());

View file

@ -14,6 +14,7 @@ const {
isEnabled,
ErrorController,
performStartupChecks,
handleJsonParseError,
initializeFileStorage,
} = require('@librechat/api');
const { connectDb, indexSync } = require('~/db');
@ -81,6 +82,7 @@ const startServer = async () => {
app.use(noIndex);
app.use(express.json({ limit: '3mb' }));
app.use(express.urlencoded({ extended: true, limit: '3mb' }));
app.use(handleJsonParseError);
app.use(mongoSanitize());
app.use(cors());
app.use(cookieParser());

View file

@ -5,6 +5,7 @@ const {
markPublicPromptGroups,
buildPromptGroupFilter,
formatPromptGroupsResponse,
safeValidatePromptGroupUpdate,
createEmptyPromptGroupsResponse,
filterAccessibleIdsBySharedLogic,
} = require('@librechat/api');
@ -344,7 +345,16 @@ const patchPromptGroup = async (req, res) => {
if (req.user.role === SystemRoles.ADMIN) {
delete filter.author;
}
const promptGroup = await updatePromptGroup(filter, req.body);
const validationResult = safeValidatePromptGroupUpdate(req.body);
if (!validationResult.success) {
return res.status(400).send({
error: 'Invalid request body',
details: validationResult.error.errors,
});
}
const promptGroup = await updatePromptGroup(filter, validationResult.data);
res.status(200).send(promptGroup);
} catch (error) {
logger.error(error);

View file

@ -544,6 +544,169 @@ describe('Prompt Routes - ACL Permissions', () => {
});
});
describe('PATCH /api/prompts/groups/:groupId - Update Prompt Group Security', () => {
let testGroup;
beforeEach(async () => {
// Create a prompt group
testGroup = await PromptGroup.create({
name: 'Security Test Group',
category: 'security-test',
author: testUsers.owner._id,
authorName: testUsers.owner.name,
productionId: new ObjectId(),
});
// Grant owner permissions
await grantPermission({
principalType: PrincipalType.USER,
principalId: testUsers.owner._id,
resourceType: ResourceType.PROMPTGROUP,
resourceId: testGroup._id,
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: testUsers.owner._id,
});
});
afterEach(async () => {
await PromptGroup.deleteMany({});
await AclEntry.deleteMany({});
});
it('should allow updating allowed fields (name, category, oneliner)', async () => {
const updateData = {
name: 'Updated Group Name',
category: 'updated-category',
oneliner: 'Updated description',
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(updateData)
.expect(200);
expect(response.body.name).toBe(updateData.name);
expect(response.body.category).toBe(updateData.category);
expect(response.body.oneliner).toBe(updateData.oneliner);
});
it('should reject request with author field (400 Bad Request)', async () => {
const maliciousUpdate = {
name: 'Legit Update',
author: testUsers.noAccess._id.toString(), // Try to change ownership
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected
expect(response.body.error).toBe('Invalid request body');
expect(response.body.details).toBeDefined();
});
it('should reject request with authorName field (400 Bad Request)', async () => {
const maliciousUpdate = {
name: 'Legit Update',
authorName: 'Malicious Author Name',
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected
expect(response.body.error).toBe('Invalid request body');
});
it('should reject request with _id field (400 Bad Request)', async () => {
const newId = new ObjectId();
const maliciousUpdate = {
name: 'Legit Update',
_id: newId.toString(),
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected
expect(response.body.error).toBe('Invalid request body');
});
it('should reject request with productionId field (400 Bad Request)', async () => {
const newProductionId = new ObjectId();
const maliciousUpdate = {
name: 'Legit Update',
productionId: newProductionId.toString(),
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected
expect(response.body.error).toBe('Invalid request body');
});
it('should reject request with createdAt field (400 Bad Request)', async () => {
const maliciousDate = new Date('2020-01-01');
const maliciousUpdate = {
name: 'Legit Update',
createdAt: maliciousDate.toISOString(),
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected
expect(response.body.error).toBe('Invalid request body');
});
it('should reject request with __v field (400 Bad Request)', async () => {
const maliciousUpdate = {
name: 'Legit Update',
__v: 999,
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected
expect(response.body.error).toBe('Invalid request body');
});
it('should reject request with multiple sensitive fields (400 Bad Request)', async () => {
const maliciousUpdate = {
name: 'Legit Update',
author: testUsers.noAccess._id.toString(),
authorName: 'Hacker',
_id: new ObjectId().toString(),
productionId: new ObjectId().toString(),
createdAt: new Date('2020-01-01').toISOString(),
__v: 999,
};
const response = await request(app)
.patch(`/api/prompts/groups/${testGroup._id}`)
.send(maliciousUpdate)
.expect(400);
// Verify the request was rejected with validation errors
expect(response.body.error).toBe('Invalid request body');
expect(response.body.details).toBeDefined();
expect(Array.isArray(response.body.details)).toBe(true);
});
});
describe('Pagination', () => {
beforeEach(async () => {
// Create multiple prompt groups for pagination testing