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
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")
dest = (project_root / folder / subdir).resolve()
# Ensure destination stays within the project root

View File

@@ -10,6 +10,7 @@ from __future__ import annotations
import hashlib
import json
import os
from datetime import datetime, timezone
from pathlib import Path
from typing import Any
@@ -147,10 +148,11 @@ class IntegrationManifest:
# Use non-resolved path for deletion so symlinks themselves
# are removed, not their targets.
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:
resolved = path.resolve()
resolved.relative_to(root)
normed = Path(os.path.normpath(path))
normed.relative_to(root)
except (ValueError, OSError):
continue
if not path.exists():