Skip to main content
Glama
ifmelate

n8n-workflow-builder-mcp

by ifmelate
path-security.test.js11.3 kB
const { describe, it, expect, beforeEach } = require('@jest/globals'); const path = require('path'); const fs = require('fs').promises; // Import the security utilities const { PathSecurityError, setWorkspaceDir, getWorkspaceDir, resolvePath, resolveWorkflowPath } = require('../../dist/utils/workspace.js'); // Mock file system jest.mock('fs', () => ({ promises: { mkdir: jest.fn().mockResolvedValue(), readFile: jest.fn(), writeFile: jest.fn(), access: jest.fn().mockResolvedValue(), stat: jest.fn() } })); describe('Path Security Features', () => { const originalCwd = process.cwd(); const testWorkspaceDir = '/Users/test/workspace'; beforeEach(() => { jest.clearAllMocks(); // Reset to a safe test workspace setWorkspaceDir(testWorkspaceDir); }); describe('PathSecurityError', () => { it('should create error with attempted path', () => { const attemptedPath = '/dangerous/path'; const error = new PathSecurityError('Test error', attemptedPath); expect(error.name).toBe('PathSecurityError'); expect(error.message).toBe('Test error'); expect(error.attemptedPath).toBe(attemptedPath); expect(error instanceof Error).toBe(true); }); }); describe('setWorkspaceDir', () => { it('should set workspace directory for valid paths', () => { const validPath = '/Users/test/project'; setWorkspaceDir(validPath); expect(getWorkspaceDir()).toBe(path.resolve(validPath)); }); it('should reject root directory on Unix', () => { expect(() => { setWorkspaceDir('/'); }).toThrow(PathSecurityError); }); it('should reject Windows root directories', () => { // Only test on non-Windows systems or mock the behavior if (process.platform === 'win32') { expect(() => { setWorkspaceDir('C:\\'); }).toThrow(PathSecurityError); expect(() => { setWorkspaceDir('C:'); }).toThrow(PathSecurityError); } else { // On Unix systems, these might not match the Windows pattern // Let's test with a path that should be rejected expect(() => { setWorkspaceDir('/'); }).toThrow(PathSecurityError); } }); it('should reject Windows drive root patterns', () => { if (process.platform === 'win32') { expect(() => { setWorkspaceDir('D:\\'); }).toThrow(PathSecurityError); } else { // Test an equivalent dangerous path on Unix expect(() => { setWorkspaceDir('/'); }).toThrow(PathSecurityError); } }); }); describe('resolvePath filename sanitization', () => { it('should sanitize dangerous characters in filenames', () => { const result = resolvePath('test<>:"|?*.txt'); expect(result).toContain('test_______.txt'); }); it('should handle path separators and traversal attempts', () => { // This should be blocked by security validation, not just sanitized expect(() => { resolvePath('test/../../evil.txt'); }).toThrow(PathSecurityError); }); it('should remove null bytes and control characters', () => { const result = resolvePath('test\x00\x01\x1f\x7f.txt'); expect(result).toContain('test.txt'); expect(result).not.toMatch(/[\x00-\x1f\x7f-\x9f]/); }); it('should handle empty filenames', () => { const result = resolvePath('...'); expect(result).toContain('unnamed'); }); it('should trim whitespace and dots', () => { const result = resolvePath(' ...test... '); expect(result).toContain('test'); expect(result).not.toMatch(/^\s+|\s+$/); }); }); describe('resolvePath security validation', () => { it('should allow valid paths within workspace', () => { const result = resolvePath('workflows/test.json'); expect(result).toBe(path.join(testWorkspaceDir, 'workflows/test.json')); }); it('should block path traversal attempts', () => { expect(() => { resolvePath('../../../etc/passwd'); }).toThrow(PathSecurityError); }); it('should block attempts to escape workspace', () => { expect(() => { resolvePath('../../outside-workspace/file.txt'); }).toThrow(PathSecurityError); }); it('should allow relative navigation within workspace', () => { const result = resolvePath('subdir/../other/file.txt'); expect(result).toBe(path.join(testWorkspaceDir, 'other/file.txt')); }); it('should block access to root after resolution', () => { // This would resolve to root if workspace was set to a shallow path setWorkspaceDir('/tmp'); expect(() => { resolvePath('../../../'); }).toThrow(PathSecurityError); }); }); describe('resolveWorkflowPath security', () => { beforeEach(() => { setWorkspaceDir(testWorkspaceDir); }); it('should sanitize workflow names', () => { const result = resolveWorkflowPath('test<>:"|?*workflow'); expect(result).toContain('test_______workflow.json'); // 7 underscores for the 7 dangerous chars }); it('should reject empty workflow names', () => { expect(() => { resolveWorkflowPath(''); }).toThrow(PathSecurityError); expect(() => { resolveWorkflowPath('...'); }).toThrow(PathSecurityError); }); it('should handle workflow names with path separators', () => { const result = resolveWorkflowPath('folder/../../evil'); expect(result).toContain('folder_.._.._evil.json'); // Actual output has 3 underscores between components expect(result).not.toContain('../'); }); describe('absolute workflow paths', () => { it('should block access to root directory', () => { expect(() => { resolveWorkflowPath('test', '/'); }).toThrow(PathSecurityError); }); it('should block access to system directories', () => { expect(() => { resolveWorkflowPath('test', '/etc/passwd'); }).toThrow(PathSecurityError); expect(() => { resolveWorkflowPath('test', '/root/secret'); }).toThrow(PathSecurityError); expect(() => { resolveWorkflowPath('test', '/sys/kernel'); }).toThrow(PathSecurityError); expect(() => { resolveWorkflowPath('test', '/proc/version'); }).toThrow(PathSecurityError); }); it('should allow valid absolute paths', () => { const validPath = '/Users/test/custom/workflow.json'; const result = resolveWorkflowPath('test', validPath); expect(result).toBe(path.resolve(validPath)); }); }); describe('relative workflow paths', () => { it('should resolve relative paths against cwd', () => { const relativePath = 'custom/workflow.json'; const result = resolveWorkflowPath('test', relativePath); expect(result).toBe(path.resolve(process.cwd(), relativePath)); }); it('should validate relative paths stay within bounds', () => { // This should be caught by path validation expect(() => { resolveWorkflowPath('test', '../../../etc/passwd'); }).toThrow(PathSecurityError); }); it('should allow valid relative paths', () => { const relativePath = 'subfolder/workflow.json'; const result = resolveWorkflowPath('test', relativePath); expect(result).toBe(path.resolve(process.cwd(), relativePath)); }); }); describe('default workflow paths', () => { it('should create secure default paths', () => { const result = resolveWorkflowPath('my-workflow'); expect(result).toBe(path.join(testWorkspaceDir, 'workflow_data/my-workflow.json')); }); it('should sanitize workflow names in default paths', () => { const result = resolveWorkflowPath('test/../../evil'); expect(result).toContain('test_.._.._evil.json'); // Actual output expect(result).not.toContain('../'); }); }); }); describe('Security edge cases', () => { it('should handle Unicode characters safely', () => { const result = resolvePath('test-файл-测试.json'); expect(result).toContain('test-файл-测试.json'); }); it('should handle very long filenames', () => { const longName = 'a'.repeat(300); const result = resolvePath(longName + '.json'); expect(result).toContain('.json'); }); it('should handle mixed path separators', () => { const result = resolvePath('folder\\subfolder/file.json'); expect(result).toContain('folder_subfolder'); // Path handling may keep directory structure expect(result).toContain('file.json'); }); it('should validate normalized paths', () => { // Path that normalizes to outside workspace expect(() => { resolvePath('normal/./../../escaping'); }).toThrow(PathSecurityError); }); }); describe('Integration with MCP tools', () => { it('should work with typical workflow names', () => { const workflowNames = [ 'my-ai-workflow', 'data_processing_2024', 'workflow.v1.2', 'test-workflow-123' ]; workflowNames.forEach(name => { const result = resolveWorkflowPath(name); expect(result).toContain(`${name}.json`); expect(result).toContain('workflow_data'); }); }); it('should handle user-provided paths safely', () => { const userPaths = [ './my-workflows/test.json', 'workflows/ai-demo.json', 'custom/path/workflow.json' ]; userPaths.forEach(userPath => { const result = resolveWorkflowPath('test', userPath); expect(result).toBe(path.resolve(process.cwd(), userPath)); }); }); }); });

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/ifmelate/n8n-workflow-builder-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server