UNPKG

mcp-sanitizer

Version:

Comprehensive security sanitization library for Model Context Protocol (MCP) servers with trusted security libraries

408 lines (338 loc) 12.4 kB
/** * 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 } }