T05 · Unauthorized Access and Privilege Escalation
Warning
- Location
- scripts/run.py:69
- Finding
- Workspace-Controlled Pipeline Lock Allows Reads Outside the Bundled Pipelines Directory<![CDATA[ ## Vulnerability Details **File Location**: `scripts/run.py:23-30, 69-78`; `tooling/pipeline_spec.py:62-64, 145-157` **Vulnerability Type**: Path traversal and unrestricted absolute-path resolution **Risk Level**: Medium ### Vulnerable Code ```python def _read_pipeline_path(workspace: Path) -> str: lock_path = workspace / "PIPELINE.lock.md" if not lock_path.exists(): return "" for raw in lock_path.read_text(encoding="utf-8", errors="ignore").splitlines(): line = raw.strip() if line.startswith("pipeline:"): return line.split(":", 1)[1].strip() return "" ``` ```python pipeline_rel = _read_pipeline_path(workspace) pipeline_path = (repo_root / pipeline_rel).resolve() if pipeline_rel else None target_artifacts: list[tuple[str, bool]] = [] pipeline_load_error = "" if pipeline_path and pipeline_path.exists(): try: from tooling.pipeline_spec import PipelineSpec spec = PipelineSpec.load(pipeline_path) ``` The resulting path is opened by the pipeline parser: ```python @staticmethod def load(path: Path) -> "PipelineSpec": resolved = path.resolve() raw_frontmatter, frontmatter = _load_variant_aware_frontmatter(resolved) ``` ```python def _load_variant_aware_frontmatter( path: Path, seen: set[Path] | None = None, ) -> tuple[dict[str, Any], dict[str, Any]]: resolved = path.resolve() active = set(seen or set()) if resolved in active: chain = " -> ".join(str(p) for p in [*active, resolved]) raise ValueError(f"Cyclic `variant_of` chain detected: {chain}") active.add(resolved) text = resolved.read_text(encoding="utf-8") raw = _parse_frontmatter(text) ``` ### Technical Analysis The `pipeline:` value is read from `PIPELINE.lock.md` inside a caller-supplied workspace. The value is joined to `repo_root` and resolved, but the resolved path is never checked to ensure that it remains beneath the expected `pipelines/` directory. `pathlib` ...[truncated 2206 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Reject absolute values from `PIPELINE.lock.md`. 2. Resolve pipeline references exclusively against the expected directory. 3. Verify containment after canonicalization: ```python pipelines_root = (repo_root / "pipelines").resolve() supplied = Path(pipeline_rel) if supplied.is_absolute(): raise ValueError("Absolute pipeline paths are not allowed") candidate = (repo_root / supplied).resolve() if not candidate.is_relative_to(pipelines_root): raise ValueError("Pipeline path must remain under the pipelines directory") if not candidate.name.endswith(".pipeline.md"): raise ValueError("Pipeline specification must end with .pipeline.md") ``` 4. Prefer resolving a pipeline identifier through the existing centralized resolver rather than directly joining the untrusted value to `repo_root`. 5. Allowlist the bundled pipeline specifications if arbitrary pipeline files are not required. 6. Avoid placing raw parser exceptions into user-facing reports. Use a generic failure message and place sanitized diagnostic details in a protected local log. 7. Apply equivalent containment validation to `variant_of` references in `tooling/pipeline_spec.py`, including rejection of absolute references. ]]>
