# REPRO-2026-00136: Microsoft APM: arbitrary file disclosure via symlink-following on apm install ## Summary Status: published Severity: high Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00136 GHSA: GHSA-q5pp-gvjg-h7v4 CVE: CVE-2026-45539 ## Package Name: apm Ecosystem: pip Affected: 0.5.4 through 0.12.4 inclusive Fixed: 0.13.0 ## Root Cause # Root Cause Analysis: CVE-2026-45539 ## Summary `apm-cli` (the `apm-cli` PyPI package, versions 0.5.4 through 0.12.4) transparently follows symbolic links when enumerating and reading APM package primitive files (`.prompt.md`, `.agent.md`, `.chatmode.md`). A malicious APM dependency can include a symlink under `.apm/prompts/` or `.apm/agents/` that points to an arbitrary absolute host path (e.g. `/etc/passwd`). During `apm install`, the CLI dereferences the symlink, reads the target file's contents with `Path.read_text()`, and copies those contents into the consuming project's installed-package tree (e.g. `.github/prompts/`). This results in arbitrary file disclosure into the victim's project tree. ## Impact - **Package/component affected**: `apm-cli` (PyPI package) - **Affected versions**: `0.5.4` → `0.12.4` (inclusive) - **Fixed versions**: `0.13.0` - **Risk level**: High (CVSS 3.1 base 7.4) - **Consequences**: A malicious dependency author can exfiltrate any file readable by the user running `apm install` into files inside the victim project, where they may be committed, uploaded, or otherwise exposed. ## Root Cause The vulnerable code paths are in `PromptIntegrator.find_prompt_files()`, `AgentIntegrator.find_agent_files()`, and the various `copy_*()` methods in `src/apm_cli/integration/`. Specifically: 1. **File enumeration** used bare `Path.glob()` and `Path.rglob()` without checking whether entries were symlinks. On Python 3.10+, `Path.glob()` transparently follows symlinks, so a symlink inside `.apm/prompts/` matching `*.prompt.md` would be returned as a valid file path. 2. **File reading** used `Path.read_text()` which dereferences symlinks. When `copy_prompt()` or agent copy methods read the source file, they inadvertently read the symlink target's contents. 3. **No containment check** verified whether a resolved file path stayed within the package directory. The fix in `v0.13.0` (commit range from `v0.12.4..v0.13.0`, security merge commits `77d1dda` and `f85b9f5`) addresses this by: - Introducing/refactoring `BaseIntegrator.find_files_by_glob()` to skip symlink entries (`f.is_symlink()`) and hardlinks (`st_nlink > 1`), plus a defense-in-depth `resolve().is_relative_to()` containment guard. - Updating `PromptIntegrator.find_prompt_files()` and `AgentIntegrator.find_agent_files()` to use `find_files_by_glob()` instead of bare `glob()`/`rglob()`. - Adding explicit `source.is_symlink()` checks in `copy_prompt()`, `copy_agent()`, and related agent copy methods that raise `ValueError` if a symlink is somehow passed through. ## Reproduction Steps The reproduction script is located at: - `repro/reproduction_steps.sh` What the script does: 1. Creates a scratch workspace under `$ROOT/scratch`. 2. Creates a malicious local APM package (`malicious_pkg`) containing: - A legitimate `.apm/prompts/legit.prompt.md` and `.apm/agents/legit.agent.md` - A symlink `.apm/prompts/evil.prompt.md -> /path/to/sentinel.txt` 3. Creates a consumer project with `target: copilot` and an empty `dependencies.apm` list. 4. Installs `apm-cli==0.12.4` (vulnerable) in a virtualenv. 5. Runs `apm install ../malicious_pkg` from the consumer project. 6. Verifies that `evil.prompt.md` appears in `.github/prompts/` with the sentinel file's contents (host file disclosure). 7. Installs `apm-cli==0.13.0` (fixed) in the same virtualenv. 8. Resets the consumer project state and re-runs `apm install ../malicious_pkg`. 9. Verifies that `evil.prompt.md` does **not** appear in `.github/prompts/`. **Expected evidence of reproduction:** - With `apm-cli==0.12.4`: The install log shows `2 prompts integrated`, and `scratch/consumer/.github/prompts/evil.prompt.md` exists containing the sentinel string. - With `apm-cli==0.13.0`: The install log shows `1 prompts integrated`, and `evil.prompt.md` is absent from `.github/prompts/`. ## Evidence Log files generated by the reproduction script: - `logs/vulnerable_install.log` — Output of `apm install` using the vulnerable build. - `logs/fixed_install.log` — Output of `apm install` using the fixed build. Key excerpts from a successful run: **Vulnerable build (0.12.4):** ``` [+] ../malicious_pkg (local) |-- 2 prompts integrated -> .github/prompts/ |-- 1 agents integrated -> .github/agents/ ``` Post-install file check: ``` FILE: .../.github/prompts/evil.prompt.md CVE-2026-45539-REPRO-SENTINEL- ``` **Fixed build (0.13.0):** ``` [+] ../malicious_pkg (local) |-- 1 prompts integrated -> .github/prompts/ |-- 1 agents integrated -> .github/agents/ ``` Post-install file check: - `evil.prompt.md` is **not** present under `.github/prompts/`. ## Recommendations / Next Steps 1. **Upgrade immediately** to `apm-cli>=0.13.0`. 2. **Audit existing projects** for any `apm_modules/` or installed dependencies that may have already leaked host file contents. 3. **Review dependency sources** — only install APM packages from trusted registries or verified local bundles. 4. **Add regression tests** that exercise symlink and hardlink rejection during integration (the project already added `tests/unit/integration/test_symlink_rejection.py` in the fix release). 5. **Consider broader symlink policy** — evaluate whether symlinks should be permitted anywhere in the install pipeline (e.g. during download/unpack) or should be stripped at package ingestion time. ## Additional Notes - **Idempotency confirmed**: The reproduction script passes two consecutive runs without modification, producing the same observable results each time. - **Edge cases**: The fix also rejects hardlinks (`st_nlink > 1`) because `Path.resolve()` returns the hardlink's own path, so `is_relative_to()` cannot catch a hardlink to a file outside the package tree. - **Limitations**: The reproduction uses a local path dependency (`../malicious_pkg`). The same vulnerability would be exploitable via remote dependencies if an attacker could publish a malicious APM package containing symlinks in its `.apm/` subtree. ## Reproduction Details Reproduced: 2026-05-22T09:39:21.610Z Duration: 1742 seconds Tool calls: 212 Turns: 152 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00136 pruva-verify GHSA-q5pp-gvjg-h7v4 pruva-verify CVE-2026-45539 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00136&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00136/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-q5pp-gvjg-h7v4 - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-45539 - Source: https://github.com/microsoft/apm ## Artifacts - repro/rca_report.md (analysis, 6016 bytes) - repro/reproduction_steps.sh (reproduction_script, 3753 bytes) - vuln_variant/rca_report.md (analysis, 7732 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 5020 bytes) - bundle/context.json (other, 4356 bytes) - bundle/metadata.json (other, 692 bytes) - bundle/ticket.md (ticket, 4630 bytes) - repro/validation_verdict.json (other, 1985 bytes) - vuln_variant/root_cause_equivalence.json (other, 1551 bytes) - vuln_variant/patch_analysis.md (documentation, 4363 bytes) - vuln_variant/variant_manifest.json (other, 2856 bytes) - vuln_variant/runtime_manifest.json (other, 1662 bytes) - vuln_variant/validation_verdict.json (other, 1583 bytes) - vuln_variant/source_identity.json (other, 639 bytes) - logs/fixed_install.log (log, 341 bytes) - logs/variant_latest_install.log (log, 333 bytes) - logs/variant_vulnerable_install.log (log, 282 bytes) - logs/vulnerable_install.log (log, 398 bytes) - logs/variant_fixed_install.log (log, 333 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00136 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00136/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00136 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://pruva.dev