Skip to main content
Glama
test_path_security.pyโ€ข13.7 kB
#!/usr/bin/env python3 """ Consolidated path security tests for Fast Apply MCP server. Tests path traversal prevention, symlink attacks, and all security-related path operations. """ import asyncio import os import shutil import sys import tempfile import unittest # Add the parent directory to sys.path to import main sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) from fastapply.main import call_tool class TestPathSecurity(unittest.TestCase): """Test path security functionality.""" def setUp(self): """Set up test environment.""" # Create a temporary directory for testing self.test_dir = tempfile.mkdtemp() self.original_workspace = os.environ.get("WORKSPACE_ROOT") os.environ["WORKSPACE_ROOT"] = self.test_dir # Create test files and directories self.safe_file = os.path.join(self.test_dir, "safe_file.txt") self.subdir = os.path.join(self.test_dir, "subdir") os.makedirs(self.subdir) with open(self.safe_file, "w") as f: f.write("safe content") def tearDown(self): """Clean up test environment.""" # Restore original workspace root if self.original_workspace: os.environ["WORKSPACE_ROOT"] = self.original_workspace else: os.environ.pop("WORKSPACE_ROOT", None) # Clean up test directory shutil.rmtree(self.test_dir) def test_secure_resolve_valid_path(self): """Test that valid paths within workspace are resolved correctly.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) test_path = "subdir/file.txt" expected = os.path.realpath(os.path.join(self.test_dir, "subdir", "file.txt")) result = main._secure_resolve(test_path) self.assertEqual(result, expected) def test_secure_resolve_parent_directory_traversal(self): """Test that parent directory traversal is blocked.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Test with a path that should definitely escape with self.assertRaises(ValueError) as cm: main._secure_resolve("../etc/passwd") self.assertIn("escapes workspace", str(cm.exception)) # Test multiple levels with self.assertRaises(ValueError) as cm: main._secure_resolve("subdir/../../../etc/passwd") self.assertIn("escapes workspace", str(cm.exception)) def test_secure_resolve_absolute_path(self): """Test that absolute paths outside workspace are blocked.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Test paths that are definitely outside the workspace malicious_paths = ["/etc/passwd", "/windows/system32/config", "/tmp/malicious"] for path in malicious_paths: with self.assertRaises(ValueError) as cm: main._secure_resolve(path) self.assertIn("escapes workspace", str(cm.exception)) def test_secure_resolve_absolute_path_within_workspace(self): """Test that absolute paths within workspace are allowed.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) abs_path = os.path.join(self.test_dir, "abs_file.txt") with open(abs_path, "w") as f: f.write("abs content") result = main._secure_resolve(abs_path) expected = os.path.realpath(abs_path) self.assertEqual(result, expected) def test_secure_resolve_symlink_attack(self): """Test that symlink attacks are mitigated.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Create a symlink outside workspace outside_file = os.path.join(os.path.dirname(self.test_dir), "outside.txt") symlink_path = os.path.join(self.test_dir, "safe_link") try: # Create the file outside workspace with open(outside_file, "w") as f: f.write("outside content") # Create the symlink os.symlink(outside_file, symlink_path) # This should be blocked by the secure resolve function with self.assertRaises(ValueError) as cm: main._secure_resolve(symlink_path) self.assertIn("escapes workspace", str(cm.exception)) except (FileNotFoundError, OSError): # If symlink creation fails, skip this test self.skipTest("Symlink creation not supported") finally: # Clean up if os.path.exists(symlink_path): os.unlink(symlink_path) if os.path.exists(outside_file): os.remove(outside_file) def test_secure_resolve_safe_symlink_handling(self): """Test handling of symlinks that stay within workspace.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Create symlink pointing within workspace target_file = os.path.join(self.test_dir, "target.txt") symlink_path = os.path.join(self.test_dir, "safe_link") with open(target_file, "w") as f: f.write("target content") try: os.symlink(target_file, symlink_path) # Should resolve successfully result = main._secure_resolve("safe_link") expected = os.path.realpath(target_file) self.assertEqual(result, expected) finally: if os.path.exists(symlink_path): os.unlink(symlink_path) def test_secure_resolve_workspace_root_exact_match(self): """Test that exact workspace root matches are allowed.""" # Test that "." resolves to the current workspace root # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) result = main._secure_resolve(".") # The function may resolve to current working directory due to CWD fallback expected_workspace = os.path.realpath(self.test_dir) expected_cwd = os.path.realpath(os.getcwd()) # Result should be either the workspace root or the current working directory self.assertIn(result, [expected_workspace, expected_cwd]) def test_secure_resolve_nonexistent_path_handling(self): """Test handling of nonexistent paths.""" # Nonexistent paths should resolve successfully (path doesn't need to exist) # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) result = main._secure_resolve("nonexistent.txt") expected = os.path.realpath(os.path.join(self.test_dir, "nonexistent.txt")) self.assertEqual(result, expected) def test_secure_resolve_empty_path_handling(self): """Test handling of empty paths.""" # Empty path should resolve to workspace root or current working directory # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) result = main._secure_resolve("") # The function may resolve to current working directory due to CWD fallback expected_workspace = os.path.realpath(self.test_dir) expected_cwd = os.path.realpath(os.getcwd()) # Result should be either the workspace root or the current working directory self.assertIn(result, [expected_workspace, expected_cwd]) def test_workspace_root_environment_variable(self): """Test that WORKSPACE_ROOT can be set via environment variable.""" custom_root = "/custom/workspace" os.environ["WORKSPACE_ROOT"] = custom_root # Reload the module to pick up new environment variable import importlib from fastapply import main importlib.reload(main) self.assertEqual(main.WORKSPACE_ROOT, custom_root) def test_mcp_call_tool_security_path_validation(self): """Test that MCP call tool validates file paths.""" # call_tool is imported at module level # Test with malicious path with self.assertRaises(ValueError) as cm: asyncio.run(call_tool("edit_file", { "target_file": "../etc/passwd", "instructions": "test", "code_edit": "test" })) self.assertIn("Invalid file path", str(cm.exception)) # Test with absolute path outside workspace with self.assertRaises(ValueError) as cm: asyncio.run(call_tool("edit_file", { "target_file": "/etc/passwd", "instructions": "test", "code_edit": "test" })) self.assertIn("Invalid file path", str(cm.exception)) def test_path_resolution_edge_cases(self): """Test edge cases in path resolution.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Test paths that resolve to the same location test_file = os.path.join(self.test_dir, "test.txt") with open(test_file, "w") as f: f.write("test content") # Test relative path variations paths_to_test = [ "test.txt", "./test.txt", "subdir/../test.txt", "subdir/./../test.txt" ] expected_result = os.path.realpath(test_file) for test_path in paths_to_test: with self.subTest(path=test_path): result = main._secure_resolve(test_path) self.assertEqual(result, expected_result) class TestPathResolutionScenarios(unittest.TestCase): """Test comprehensive path resolution scenarios.""" def setUp(self): """Set up test environment.""" self.test_dir = tempfile.mkdtemp() self.original_workspace = os.environ.get("WORKSPACE_ROOT") os.environ["WORKSPACE_ROOT"] = self.test_dir def tearDown(self): """Clean up test environment.""" if self.original_workspace: os.environ["WORKSPACE_ROOT"] = self.original_workspace else: os.environ.pop("WORKSPACE_ROOT", None) shutil.rmtree(self.test_dir) def test_path_escaping_workspace(self): """Test various path escaping scenarios.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) escape_attempts = [ "../etc/passwd", "../../etc/passwd", "../../../etc/passwd", "subdir/../../../etc/passwd", "./../etc/passwd", "subdir/../subdir2/../../etc/passwd" ] for attempt in escape_attempts: with self.subTest(path=attempt): with self.assertRaises(ValueError) as cm: main._secure_resolve(attempt) self.assertIn("escapes workspace", str(cm.exception)) def test_symlink_handling_edge_cases(self): """Test symlink edge cases including broken symlinks.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Create broken symlink broken_symlink = os.path.join(self.test_dir, "broken_link") try: os.symlink("/nonexistent/target", broken_symlink) # Should still validate that target would escape workspace with self.assertRaises(ValueError) as cm: main._secure_resolve("broken_link") self.assertIn("escapes workspace", str(cm.exception)) except (FileNotFoundError, OSError): self.skipTest("Symlink creation not supported") finally: if os.path.exists(broken_symlink): os.unlink(broken_symlink) def test_complex_path_scenarios(self): """Test complex path scenarios with mixed operations.""" # Need to reload the module to pick up the new environment variable import importlib from fastapply import main importlib.reload(main) # Create nested directory structure nested_dir = os.path.join(self.test_dir, "deep", "nested", "structure") os.makedirs(nested_dir) # Test paths that should work valid_paths = [ "deep/nested/structure", "./deep/nested/structure", "deep/./nested/structure", "deep/nested/../nested/structure" ] expected = os.path.realpath(nested_dir) for path in valid_paths: with self.subTest(path=path): result = main._secure_resolve(path) self.assertEqual(result, expected) if __name__ == "__main__": unittest.main(verbosity=2)

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/betmoar/FastApply-MCP'

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