fix: lexical symlink containment, assert project_root consistency

- uninstall() now uses os.path.normpath for lexical containment check
  instead of resolve(), so in-project symlinks pointing outside are
  still properly removed
- setup() asserts manifest.project_root matches the passed project_root
  to prevent path mismatches between file operations and manifest
  recording
This commit is contained in:
Manfred Riem
2026-03-31 09:41:33 -05:00
parent 7ccbf6913a
commit a2f03ceacf
2 changed files with 10 additions and 3 deletions

View File

@@ -120,6 +120,11 @@ class IntegrationBase(ABC):
return created return created
project_root_resolved = project_root.resolve() project_root_resolved = project_root.resolve()
if manifest.project_root != project_root_resolved:
raise ValueError(
f"manifest.project_root ({manifest.project_root}) does not match "
f"project_root ({project_root_resolved})"
)
subdir = self.config.get("commands_subdir", "commands") subdir = self.config.get("commands_subdir", "commands")
dest = (project_root / folder / subdir).resolve() dest = (project_root / folder / subdir).resolve()
# Ensure destination stays within the project root # Ensure destination stays within the project root

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import hashlib import hashlib
import json import json
import os
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
from typing import Any from typing import Any
@@ -147,10 +148,11 @@ class IntegrationManifest:
# Use non-resolved path for deletion so symlinks themselves # Use non-resolved path for deletion so symlinks themselves
# are removed, not their targets. # are removed, not their targets.
path = root / rel path = root / rel
# Validate containment via the resolved path # Validate containment lexically (without following symlinks)
# by collapsing .. segments via Path resolution on the string parts.
try: try:
resolved = path.resolve() normed = Path(os.path.normpath(path))
resolved.relative_to(root) normed.relative_to(root)
except (ValueError, OSError): except (ValueError, OSError):
continue continue
if not path.exists(): if not path.exists():