mcp-sanitizer
Version:
Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries
408 lines (338 loc) • 12.6 kB
JavaScript
/**
* Test file for MCP Sanitizer Middleware
*
* This test file demonstrates and validates the middleware functionality
* across different Node.js frameworks (Express, Fastify, Koa).
*/
const { describe, it, expect, beforeEach } = require('@jest/globals'); // afterEach unused
// const request = require('supertest') // Unused - commented to fix ESLint
const MCPSanitizer = require('../../src/index');
// Mock framework apps for testing
let mockExpress, mockFastify, mockKoa;
// Import middleware modules
const middlewareModule = require('../../src/middleware');
const { express, fastify, koa } = middlewareModule;
describe('MCP Sanitizer Middleware', () => {
beforeEach(() => {
// Reset mocks before each test
mockExpress = createMockExpressApp();
mockFastify = createMockFastifyApp();
mockKoa = createMockKoaApp();
});
describe('Framework Detection', () => {
it('should detect Express app correctly', () => {
const framework = middlewareModule.detectFramework(mockExpress);
expect(framework).toBe('express');
});
it('should detect Fastify app correctly', () => {
const framework = middlewareModule.detectFramework(mockFastify);
expect(framework).toBe('fastify');
});
it('should detect Koa app correctly', () => {
const framework = middlewareModule.detectFramework(mockKoa);
expect(framework).toBe('koa');
});
it('should return null for unknown framework', () => {
const framework = middlewareModule.detectFramework({ unknown: true });
expect(framework).toBe(null);
});
});
describe('Configuration Validation', () => {
it('should validate and normalize configuration', () => {
const config = {
mode: 'invalid',
policy: 'INVALID',
blockStatusCode: 999
};
const validated = middlewareModule.validateConfig(config);
expect(validated.mode).toBe('sanitize');
expect(validated.policy).toBe('PRODUCTION');
expect(validated.blockStatusCode).toBe(400);
});
it('should preserve valid configuration', () => {
const config = {
mode: 'block',
policy: 'STRICT',
blockStatusCode: 403
};
const validated = middlewareModule.validateConfig(config);
expect(validated.mode).toBe('block');
expect(validated.policy).toBe('STRICT');
expect(validated.blockStatusCode).toBe(403);
});
});
describe('Express Middleware', () => {
it('should create Express middleware with default configuration', () => {
const middleware = express.createExpressMiddleware();
expect(typeof middleware).toBe('function');
expect(middleware.length).toBe(3); // req, res, next
});
it('should sanitize request body in sanitize mode', () => {
const onWarning = jest.fn();
const middleware = express.createExpressMiddleware({
mode: 'sanitize',
onWarning
});
const mockReq = createMockExpressRequest({
body: {
command: 'ls; rm -rf /', // Malicious command
file_path: 'normal.txt'
}
});
const mockRes = createMockExpressResponse();
const mockNext = jest.fn();
middleware(mockReq, mockRes, mockNext);
// Should proceed to next middleware
expect(mockNext).toHaveBeenCalled();
// Should have sanitization warnings added to request
expect(mockReq.sanitizationWarnings).toBeDefined();
});
it('should block request in block mode with malicious content', () => {
const onBlocked = jest.fn(() => false); // Return false to prevent default response
const middleware = express.createExpressMiddleware({
mode: 'block',
onBlocked
});
const mockReq = createMockExpressRequest({
body: {
command: 'ls; rm -rf /' // Malicious command
}
});
const mockRes = createMockExpressResponse();
const mockNext = jest.fn();
middleware(mockReq, mockRes, mockNext);
// Should not proceed to next middleware
expect(mockNext).not.toHaveBeenCalled();
// Should call onBlocked callback
expect(onBlocked).toHaveBeenCalled();
});
it('should skip health check requests', () => {
const middleware = express.createExpressMiddleware({
skipHealthChecks: true
});
const mockReq = createMockExpressRequest({ path: '/health' });
const mockRes = createMockExpressResponse();
const mockNext = jest.fn();
middleware(mockReq, mockRes, mockNext);
// Should skip processing and go to next
expect(mockNext).toHaveBeenCalled();
});
it('should create MCP tool-specific middleware', () => {
const toolMiddleware = express.createMCPToolMiddleware({
toolSpecificSanitization: true
});
expect(typeof toolMiddleware).toBe('function');
const mockReq = createMockExpressRequest({
params: { toolName: 'file_reader' },
body: {
parameters: {
file_path: '../../../etc/passwd' // Path traversal attempt
}
}
});
const mockRes = createMockExpressResponse();
const mockNext = jest.fn();
toolMiddleware(mockReq, mockRes, mockNext);
expect(mockReq.mcpContext).toBeDefined();
expect(mockReq.mcpContext.toolName).toBe('file_reader');
});
});
describe('Fastify Plugin', () => {
it('should be a valid Fastify plugin function', () => {
expect(typeof fastify).toBe('function');
// Fastify plugins should be async functions
expect(fastify.constructor.name).toBe('AsyncFunction');
});
it('should have plugin metadata', () => {
// Check for fastify-plugin metadata
expect(fastify[Symbol.for('skip-override')]).toBe(true);
expect(fastify[Symbol.for('plugin-meta')]).toBeDefined();
});
it('should register hooks on Fastify instance', async () => {
const mockFastifyInstance = createMockFastifyApp();
const options = { policy: 'PRODUCTION' };
await fastify(mockFastifyInstance, options);
// Verify hooks were added
expect(mockFastifyInstance.addHook).toHaveBeenCalled();
expect(mockFastifyInstance.decorateRequest).toHaveBeenCalled();
});
});
describe('Koa Middleware', () => {
it('should create Koa middleware with default configuration', () => {
const middleware = koa.createKoaMiddleware();
expect(typeof middleware).toBe('function');
expect(middleware.constructor.name).toBe('AsyncFunction');
});
it('should sanitize request context in sanitize mode', async () => {
const onWarning = jest.fn();
const middleware = koa.createKoaMiddleware({
mode: 'sanitize',
onWarning
});
const mockCtx = createMockKoaContext({
request: {
body: {
query: 'SELECT * FROM users; DROP TABLE users;' // SQL injection
}
}
});
const mockNext = jest.fn().mockResolvedValue();
await middleware(mockCtx, mockNext);
expect(mockNext).toHaveBeenCalled();
expect(mockCtx.sanitizationWarnings).toBeDefined();
});
it('should add sanitization data to state', async () => {
const middleware = koa.createKoaMiddleware({
addToState: true,
contextKey: 'mcp'
});
const mockCtx = createMockKoaContext({
request: { body: { test: 'safe content' } },
state: {}
});
const mockNext = jest.fn().mockResolvedValue();
await middleware(mockCtx, mockNext);
expect(mockCtx.state.mcp).toBeDefined();
expect(mockCtx.state.mcp.processed).toBe(true);
});
it('should create MCP server middleware', () => {
const serverMiddleware = koa.createMCPServerMiddleware({
toolSpecificSanitization: true
});
expect(typeof serverMiddleware).toBe('function');
expect(serverMiddleware.constructor.name).toBe('AsyncFunction');
});
});
describe('Universal Middleware Factory', () => {
it('should create universal middleware with all frameworks', () => {
const universal = middlewareModule.createUniversalMiddleware({
policy: 'PRODUCTION',
mode: 'block'
});
expect(universal.express).toBeDefined();
expect(universal.fastify).toBeDefined();
expect(universal.koa).toBeDefined();
expect(universal.auto).toBeDefined();
expect(universal.tool).toBeDefined();
});
it('should auto-detect framework and return appropriate middleware', () => {
const universal = middlewareModule.createUniversalMiddleware();
const expressMiddleware = universal.auto(mockExpress);
const koaMiddleware = universal.auto(mockKoa);
expect(typeof expressMiddleware).toBe('function');
expect(typeof koaMiddleware).toBe('function');
expect(expressMiddleware.length).toBe(3); // Express: req, res, next
expect(koaMiddleware.constructor.name).toBe('AsyncFunction'); // Koa: async
});
});
describe('Environment-specific Configuration', () => {
it('should create development environment middleware', () => {
const devMiddleware = middlewareModule.createEnvironmentMiddleware('development');
expect(devMiddleware.express).toBeDefined();
expect(devMiddleware.fastify).toBeDefined();
expect(devMiddleware.koa).toBeDefined();
});
it('should create production environment middleware', () => {
const prodMiddleware = middlewareModule.createEnvironmentMiddleware('production');
expect(prodMiddleware.express).toBeDefined();
expect(prodMiddleware.fastify).toBeDefined();
expect(prodMiddleware.koa).toBeDefined();
});
it('should apply environment-specific defaults', () => {
const testMiddleware = middlewareModule.createEnvironmentMiddleware('testing', {
customOption: true
});
expect(testMiddleware).toBeDefined();
});
});
describe('Tool-specific Sanitization', () => {
it('should handle file_reader tool parameters', () => {
const mockReq = createMockExpressRequest({
params: { toolName: 'file_reader' },
body: {
parameters: {
file_path: '../../../etc/passwd'
}
}
});
// This would normally be called internally by the middleware
const sanitizer = new MCPSanitizer({ policy: 'PRODUCTION' });
const result = sanitizer.sanitize(mockReq.body.parameters.file_path, { type: 'file_path' });
expect(result.warnings.length).toBeGreaterThan(0);
expect(result.warnings.some(w => typeof w === 'string' && w.includes('blocked pattern'))).toBe(true);
});
it('should handle command execution tool parameters', () => {
const mockReq = createMockExpressRequest({
params: { toolName: 'shell_executor' },
body: {
parameters: {
command: 'ls; rm -rf /'
}
}
});
const sanitizer = new MCPSanitizer({ policy: 'PRODUCTION' });
const result = sanitizer.sanitize(mockReq.body.parameters.command, { type: 'command' });
expect(result.warnings.length).toBeGreaterThan(0);
expect(result.warnings.some(w => typeof w === 'string' && w.includes('blocked pattern'))).toBe(true);
});
});
});
// Mock helper functions
function createMockExpressApp () {
return {
use: jest.fn(),
get: jest.fn(),
post: jest.fn(),
constructor: { name: 'Function' }
};
}
function createMockFastifyApp () {
return {
register: jest.fn(),
addHook: jest.fn(),
decorateRequest: jest.fn(),
decorate: jest.fn(),
setSchemaCompiler: jest.fn()
};
}
function createMockKoaApp () {
return {
use: jest.fn(),
constructor: { name: 'Application' }
};
}
function createMockExpressRequest (overrides = {}) {
return {
ip: '127.0.0.1',
path: '/api/test',
method: 'POST',
body: {},
params: {},
query: {},
headers: { 'user-agent': 'test-agent' },
get: jest.fn((header) => 'test-value'),
...overrides
};
}
function createMockExpressResponse () {
const res = {
status: jest.fn().mockReturnThis(),
json: jest.fn().mockReturnThis(),
send: jest.fn().mockReturnThis()
};
return res;
}
function createMockKoaContext (overrides = {}) {
return {
ip: '127.0.0.1',
path: '/api/test',
method: 'POST',
request: { body: {} },
params: {},
query: {},
headers: { 'user-agent': 'test-agent' },
get: jest.fn((header) => 'test-value'),
state: {},
...overrides
};
}