mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-03 00:58:50 +01:00
🛡️ 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:
parent
6fa94d3eb8
commit
01413eea3d
10 changed files with 653 additions and 1 deletions
|
|
@ -1,3 +1,4 @@
|
|||
export * from './access';
|
||||
export * from './error';
|
||||
export * from './balance';
|
||||
export * from './json';
|
||||
|
|
|
|||
158
packages/api/src/middleware/json.spec.ts
Normal file
158
packages/api/src/middleware/json.spec.ts
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
import { handleJsonParseError } from './json';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
describe('handleJsonParseError', () => {
|
||||
let req: Partial<Request>;
|
||||
let res: Partial<Response>;
|
||||
let next: NextFunction;
|
||||
let jsonSpy: jest.Mock;
|
||||
let statusSpy: jest.Mock;
|
||||
|
||||
beforeEach(() => {
|
||||
req = {
|
||||
path: '/api/test',
|
||||
method: 'POST',
|
||||
ip: '127.0.0.1',
|
||||
};
|
||||
|
||||
jsonSpy = jest.fn();
|
||||
statusSpy = jest.fn().mockReturnValue({ json: jsonSpy });
|
||||
|
||||
res = {
|
||||
status: statusSpy,
|
||||
json: jsonSpy,
|
||||
};
|
||||
|
||||
next = jest.fn();
|
||||
});
|
||||
|
||||
describe('JSON parse errors', () => {
|
||||
it('should handle JSON SyntaxError with 400 status', () => {
|
||||
const err = new SyntaxError('Unexpected token < in JSON at position 0') as SyntaxError & {
|
||||
status?: number;
|
||||
body?: unknown;
|
||||
};
|
||||
err.status = 400;
|
||||
err.body = {};
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledWith(400);
|
||||
expect(jsonSpy).toHaveBeenCalledWith({
|
||||
error: 'Invalid JSON format',
|
||||
message: 'The request body contains malformed JSON',
|
||||
});
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should not reflect user input in error message', () => {
|
||||
const maliciousInput = '<script>alert("xss")</script>';
|
||||
const err = new SyntaxError(
|
||||
`Unexpected token < in JSON at position 0: ${maliciousInput}`,
|
||||
) as SyntaxError & {
|
||||
status?: number;
|
||||
body?: unknown;
|
||||
};
|
||||
err.status = 400;
|
||||
err.body = maliciousInput;
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledWith(400);
|
||||
const errorResponse = jsonSpy.mock.calls[0][0];
|
||||
expect(errorResponse.message).not.toContain(maliciousInput);
|
||||
expect(errorResponse.message).toBe('The request body contains malformed JSON');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle JSON parse error with HTML tags in body', () => {
|
||||
const err = new SyntaxError('Invalid JSON') as SyntaxError & {
|
||||
status?: number;
|
||||
body?: unknown;
|
||||
};
|
||||
err.status = 400;
|
||||
err.body = '<html><body><h1>XSS</h1></body></html>';
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(statusSpy).toHaveBeenCalledWith(400);
|
||||
const errorResponse = jsonSpy.mock.calls[0][0];
|
||||
expect(errorResponse.message).not.toContain('<html>');
|
||||
expect(errorResponse.message).not.toContain('<script>');
|
||||
expect(next).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('non-JSON errors', () => {
|
||||
it('should pass through non-SyntaxError errors', () => {
|
||||
const err = new Error('Some other error');
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(err);
|
||||
expect(statusSpy).not.toHaveBeenCalled();
|
||||
expect(jsonSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass through SyntaxError without status 400', () => {
|
||||
const err = new SyntaxError('Some syntax error') as SyntaxError & { status?: number };
|
||||
err.status = 500;
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(err);
|
||||
expect(statusSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass through SyntaxError without body property', () => {
|
||||
const err = new SyntaxError('Some syntax error') as SyntaxError & { status?: number };
|
||||
err.status = 400;
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(err);
|
||||
expect(statusSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should pass through TypeError', () => {
|
||||
const err = new TypeError('Type error');
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
expect(next).toHaveBeenCalledWith(err);
|
||||
expect(statusSpy).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('security verification', () => {
|
||||
it('should return generic error message for all JSON parse errors', () => {
|
||||
const testCases = [
|
||||
'Unexpected token < in JSON',
|
||||
'Unexpected end of JSON input',
|
||||
'Invalid or unexpected token',
|
||||
'<script>alert(1)</script>',
|
||||
'"><img src=x onerror=alert(1)>',
|
||||
];
|
||||
|
||||
testCases.forEach((errorMsg) => {
|
||||
const err = new SyntaxError(errorMsg) as SyntaxError & {
|
||||
status?: number;
|
||||
body?: unknown;
|
||||
};
|
||||
err.status = 400;
|
||||
err.body = errorMsg;
|
||||
|
||||
jsonSpy.mockClear();
|
||||
statusSpy.mockClear();
|
||||
(next as jest.Mock).mockClear();
|
||||
|
||||
handleJsonParseError(err, req as Request, res as Response, next);
|
||||
|
||||
const errorResponse = jsonSpy.mock.calls[0][0];
|
||||
// Verify the generic message is always returned, not the user input
|
||||
expect(errorResponse.message).toBe('The request body contains malformed JSON');
|
||||
expect(errorResponse.error).toBe('Invalid JSON format');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
40
packages/api/src/middleware/json.ts
Normal file
40
packages/api/src/middleware/json.ts
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import { logger } from '@librechat/data-schemas';
|
||||
import type { Request, Response, NextFunction } from 'express';
|
||||
|
||||
/**
|
||||
* Middleware to handle JSON parsing errors from express.json()
|
||||
* Prevents user input from being reflected in error messages (XSS prevention)
|
||||
*
|
||||
* This middleware should be placed immediately after express.json() middleware.
|
||||
*
|
||||
* @param err - Error object from express.json()
|
||||
* @param req - Express request object
|
||||
* @param res - Express response object
|
||||
* @param next - Express next function
|
||||
*
|
||||
* @example
|
||||
* app.use(express.json({ limit: '3mb' }));
|
||||
* app.use(handleJsonParseError);
|
||||
*/
|
||||
export function handleJsonParseError(
|
||||
err: Error & { status?: number; body?: unknown },
|
||||
req: Request,
|
||||
res: Response,
|
||||
next: NextFunction,
|
||||
): void {
|
||||
if (err instanceof SyntaxError && err.status === 400 && 'body' in err) {
|
||||
logger.warn('[JSON Parse Error] Invalid JSON received', {
|
||||
path: req.path,
|
||||
method: req.method,
|
||||
ip: req.ip,
|
||||
});
|
||||
|
||||
res.status(400).json({
|
||||
error: 'Invalid JSON format',
|
||||
message: 'The request body contains malformed JSON',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
next(err);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue