
- Add PathValidator class for preventing path traversal attacks - Add SecureSubprocessRunner for safe command execution - Replace unsafe XML parsing with defusedxml for security - Add comprehensive input validation tools for circuit generation - Include security dependencies (defusedxml, bandit) in pyproject.toml - Add security scanning job to CI/CD pipeline - Add comprehensive test coverage for security utilities - Add timeout constants for safe operation limits - Add boundary validation for component positioning This establishes a strong security foundation for the KiCad MCP server by implementing defense-in-depth security measures across all input vectors and external process interactions. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
227 lines
7.2 KiB
Python
227 lines
7.2 KiB
Python
"""
|
|
Path validation utility for KiCad MCP.
|
|
|
|
Provides secure path validation to prevent path traversal attacks
|
|
and ensure file operations are restricted to safe directories.
|
|
"""
|
|
|
|
import os
|
|
import pathlib
|
|
|
|
from kicad_mcp.config import KICAD_EXTENSIONS
|
|
|
|
|
|
class PathValidationError(Exception):
|
|
"""Raised when path validation fails."""
|
|
|
|
pass
|
|
|
|
|
|
class PathValidator:
|
|
"""
|
|
Validates file paths for security and correctness.
|
|
|
|
Prevents path traversal attacks and ensures files are within
|
|
trusted directories with valid KiCad extensions.
|
|
"""
|
|
|
|
def __init__(self, trusted_roots: set[str] | None = None):
|
|
"""
|
|
Initialize path validator.
|
|
|
|
Args:
|
|
trusted_roots: Set of trusted root directories. If None,
|
|
uses current working directory.
|
|
"""
|
|
self.trusted_roots = trusted_roots or {os.getcwd()}
|
|
# Normalize trusted roots to absolute paths
|
|
self.trusted_roots = {
|
|
os.path.realpath(os.path.expanduser(root)) for root in self.trusted_roots
|
|
}
|
|
|
|
def add_trusted_root(self, root_path: str) -> None:
|
|
"""
|
|
Add a trusted root directory.
|
|
|
|
Args:
|
|
root_path: Path to add as trusted root
|
|
"""
|
|
normalized_root = os.path.realpath(os.path.expanduser(root_path))
|
|
self.trusted_roots.add(normalized_root)
|
|
|
|
def validate_path(self, file_path: str, must_exist: bool = False) -> str:
|
|
"""
|
|
Validate a file path for security and correctness.
|
|
|
|
Args:
|
|
file_path: Path to validate
|
|
must_exist: Whether the file must exist
|
|
|
|
Returns:
|
|
Normalized absolute path
|
|
|
|
Raises:
|
|
PathValidationError: If path validation fails
|
|
"""
|
|
if not file_path or not isinstance(file_path, str):
|
|
raise PathValidationError("Path must be a non-empty string")
|
|
|
|
try:
|
|
# Expand user home directory and resolve symbolic links
|
|
normalized_path = os.path.realpath(os.path.expanduser(file_path))
|
|
except (OSError, ValueError) as e:
|
|
raise PathValidationError(f"Invalid path: {e}") from e
|
|
|
|
# Check if path is within trusted roots
|
|
if not self._is_within_trusted_roots(normalized_path):
|
|
raise PathValidationError(f"Path '{file_path}' is outside trusted directories")
|
|
|
|
# Check if file exists when required
|
|
if must_exist and not os.path.exists(normalized_path):
|
|
raise PathValidationError(f"Path does not exist: {file_path}")
|
|
|
|
return normalized_path
|
|
|
|
def validate_kicad_file(self, file_path: str, file_type: str, must_exist: bool = True) -> str:
|
|
"""
|
|
Validate a KiCad file path with extension checking.
|
|
|
|
Args:
|
|
file_path: Path to validate
|
|
file_type: Expected KiCad file type ('project', 'schematic', 'pcb', etc.)
|
|
must_exist: Whether the file must exist
|
|
|
|
Returns:
|
|
Normalized absolute path
|
|
|
|
Raises:
|
|
PathValidationError: If path validation fails
|
|
"""
|
|
# First validate the basic path
|
|
normalized_path = self.validate_path(file_path, must_exist)
|
|
|
|
# Check file extension
|
|
if file_type not in KICAD_EXTENSIONS:
|
|
raise PathValidationError(f"Unknown KiCad file type: {file_type}")
|
|
|
|
expected_extension = KICAD_EXTENSIONS[file_type]
|
|
if not normalized_path.endswith(expected_extension):
|
|
raise PathValidationError(
|
|
f"File must have {expected_extension} extension, got: {file_path}"
|
|
)
|
|
|
|
return normalized_path
|
|
|
|
def validate_directory(self, dir_path: str, must_exist: bool = True) -> str:
|
|
"""
|
|
Validate a directory path.
|
|
|
|
Args:
|
|
dir_path: Directory path to validate
|
|
must_exist: Whether the directory must exist
|
|
|
|
Returns:
|
|
Normalized absolute directory path
|
|
|
|
Raises:
|
|
PathValidationError: If validation fails
|
|
"""
|
|
normalized_path = self.validate_path(dir_path, must_exist)
|
|
|
|
if must_exist and not os.path.isdir(normalized_path):
|
|
raise PathValidationError(f"Path is not a directory: {dir_path}")
|
|
|
|
return normalized_path
|
|
|
|
def validate_project_directory(self, project_path: str) -> str:
|
|
"""
|
|
Validate and return the directory containing a KiCad project file.
|
|
|
|
Args:
|
|
project_path: Path to .kicad_pro file
|
|
|
|
Returns:
|
|
Normalized absolute directory path
|
|
|
|
Raises:
|
|
PathValidationError: If validation fails
|
|
"""
|
|
validated_project = self.validate_kicad_file(project_path, "project", must_exist=True)
|
|
return os.path.dirname(validated_project)
|
|
|
|
def create_safe_temp_path(self, base_name: str, extension: str = "") -> str:
|
|
"""
|
|
Create a safe temporary file path within trusted directories.
|
|
|
|
Args:
|
|
base_name: Base name for the temporary file
|
|
extension: File extension (including dot)
|
|
|
|
Returns:
|
|
Safe temporary file path
|
|
"""
|
|
import tempfile
|
|
|
|
# Use the first trusted root as temp directory base
|
|
temp_root = next(iter(self.trusted_roots))
|
|
|
|
# Create temp directory if it doesn't exist
|
|
temp_dir = os.path.join(temp_root, "temp")
|
|
os.makedirs(temp_dir, exist_ok=True)
|
|
|
|
# Generate unique temp file path
|
|
temp_fd, temp_path = tempfile.mkstemp(
|
|
suffix=extension, prefix=f"{base_name}_", dir=temp_dir
|
|
)
|
|
os.close(temp_fd) # Close the file descriptor, we just need the path
|
|
|
|
return temp_path
|
|
|
|
def _is_within_trusted_roots(self, path: str) -> bool:
|
|
"""
|
|
Check if a path is within any trusted root directory.
|
|
|
|
Args:
|
|
path: Normalized absolute path to check
|
|
|
|
Returns:
|
|
True if path is within trusted roots
|
|
"""
|
|
for root in self.trusted_roots:
|
|
try:
|
|
# Check if path is within this root
|
|
pathlib.Path(root).resolve()
|
|
pathlib.Path(path).resolve().relative_to(pathlib.Path(root).resolve())
|
|
return True
|
|
except ValueError:
|
|
# Path is not relative to this root
|
|
continue
|
|
return False
|
|
|
|
|
|
# Global default validator instance
|
|
_default_validator = None
|
|
|
|
|
|
def get_default_validator() -> PathValidator:
|
|
"""Get the default global path validator instance."""
|
|
global _default_validator
|
|
if _default_validator is None:
|
|
_default_validator = PathValidator()
|
|
return _default_validator
|
|
|
|
|
|
def validate_path(file_path: str, must_exist: bool = False) -> str:
|
|
"""Convenience function using default validator."""
|
|
return get_default_validator().validate_path(file_path, must_exist)
|
|
|
|
|
|
def validate_kicad_file(file_path: str, file_type: str, must_exist: bool = True) -> str:
|
|
"""Convenience function using default validator."""
|
|
return get_default_validator().validate_kicad_file(file_path, file_type, must_exist)
|
|
|
|
|
|
def validate_directory(dir_path: str, must_exist: bool = True) -> str:
|
|
"""Convenience function using default validator."""
|
|
return get_default_validator().validate_directory(dir_path, must_exist)
|