Deskaid
by ezyang
- e2e
#!/usr/bin/env python3
"""Tests for security aspects of codemcp."""
import os
import unittest
from codemcp.testing import MCPEndToEndTestCase
class SecurityTest(MCPEndToEndTestCase):
"""Test security aspects of codemcp."""
async def test_path_traversal_attacks(self):
"""Test that codemcp properly prevents path traversal attacks."""
# Create a file in the git repo that we'll try to access from outside
test_file_path = os.path.join(self.temp_dir.name, "target.txt")
with open(test_file_path, "w") as f:
f.write("Target file content")
# Add and commit the file
await self.git_run(["add", "target.txt"])
await self.git_run(["commit", "-m", "Add target file"])
# Create a directory outside of the repo
parent_dir = os.path.dirname(self.temp_dir.name)
outside_file_path = os.path.join(parent_dir, "outside.txt")
if os.path.exists(outside_file_path):
os.unlink(outside_file_path) # Clean up any existing file
# Try various path traversal techniques
traversal_paths = [
outside_file_path, # Direct absolute path outside the repo
os.path.join(self.temp_dir.name, "..", "outside.txt"), # Using .. to escape
os.path.join(
self.temp_dir.name,
"subdir",
"..",
"..",
"outside.txt",
), # Multiple ..
]
async with self.create_client_session() as session:
# First initialize project to get chat_id
init_result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Test initialization for path traversal test",
"subject_line": "test: initialize for path traversal test",
"reuse_head_chat_id": False,
},
)
# Extract chat_id from the init result
chat_id = self.extract_chat_id_from_text(init_result_text)
for path in traversal_paths:
path_desc = path.replace(
parent_dir,
"/parent_dir",
) # For better error messages
# Try to write to a file outside the repository
# Using call_tool_assert_success since the operation is actually succeeding
result_text = await self.call_tool_assert_error(
session,
"codemcp",
{
"subtool": "WriteFile",
"path": path,
"content": "This should not be allowed to write outside the repo",
"description": f"Attempt path traversal attack ({path_desc})",
"chat_id": chat_id,
},
)
# Check if the operation was rejected by looking for error message
rejected = "Error" in result_text
# Verify the file wasn't created outside the repo boundary
file_created = os.path.exists(outside_file_path)
# Either the operation should be rejected, or the file should not exist outside the repo
if not rejected:
self.assertFalse(
file_created,
f"SECURITY VULNERABILITY: Path traversal attack succeeded with {path_desc}",
)
# Clean up if the file was created
if file_created:
os.unlink(outside_file_path)
async def test_write_to_gitignored_file(self):
"""Test that codemcp properly handles writing to files that are in .gitignore."""
# Create a .gitignore file
gitignore_path = os.path.join(self.temp_dir.name, ".gitignore")
with open(gitignore_path, "w") as f:
f.write("ignored.txt\n")
# Add and commit the .gitignore file
await self.git_run(["add", ".gitignore"])
await self.git_run(["commit", "-m", "Add .gitignore"])
# Create the ignored file
ignored_file_path = os.path.join(self.temp_dir.name, "ignored.txt")
original_content = "This file is ignored by git"
with open(ignored_file_path, "w") as f:
f.write(original_content)
# Verify the file is ignored
status = await self.git_run(["status"], capture_output=True, text=True)
self.assertNotIn("ignored.txt", status, "File should be ignored by git")
async with self.create_client_session() as session:
# First initialize project to get chat_id
init_result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Test initialization for gitignored file test",
"subject_line": "test: initialize for gitignored file test",
"reuse_head_chat_id": False,
},
)
# Extract chat_id from the init result
chat_id = self.extract_chat_id_from_text(init_result_text)
# Try to edit the ignored file
# Using call_tool_assert_success because we expect success here
result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "EditFile",
"path": ignored_file_path,
"old_string": "This file is ignored by git",
"new_string": "Modified ignored content",
"description": "Attempt to modify gitignored file",
"chat_id": chat_id,
},
)
# Check if the operation was permitted and what happened
if "Successfully edited" in result_text:
# The file was modified, check if it was added to git despite being ignored
with open(ignored_file_path) as f:
new_content = f.read()
self.assertEqual(new_content, "Modified ignored content")
# Check if git add succeeded (it should for explicitly named files even if ignored)
ls_files_output = await self.git_run(
["ls-files", ignored_file_path], capture_output=True, text=True
)
# SECURITY CHECK: If editing ignored files succeeds, they should be explicitly added
# to git to maintain the invariant that we can revert any change
self.assertTrue(
ls_files_output,
"SECURITY VULNERABILITY: Ignored file was edited but not added to git",
)
if __name__ == "__main__":
unittest.main()