GitHub Security Advisory: GHSA-xhrw-5qxx-jpwr
Package: apm-cli (PIP)
Severity: HIGH (CVSS 7.1)
CWE: CWE-22: Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal'), CWE-73: External Control of File Name or Path
Vulnerable versions: <= 0.8.11
Patched in: 0.8.12

Summary: Microsoft APM CLI's plugin.json component paths escape plugin root and copy arbitrary host files during install

Details:
### Summary
Microsoft APM normalizes marketplace plugins by copying plugin components referenced in `plugin.json` into `.apm/`. The manifest fields `agents`, `skills`, `commands`, and `hooks` are attacker-controlled, but the implementation does not enforce that those paths remain inside the plugin directory. A malicious plugin can therefore use absolute paths or `../` traversal paths to copy arbitrary readable host files or directories from the installer's machine during `apm install`.

In the verified primary proof of concept, a malicious plugin sets `plugin.json.commands` to an external markdown file. A single `apm install` copies that outside file into `.apm/prompts/` and then auto-integrates it into `.github/prompts/secret.prompt.md` in the victim project. This is a local supply-chain trust-boundary violation with direct confidentiality and integrity impact.

Reviewed version and commit:

- `apm-cli` version `0.8.11`
- `main` commit `70b34faa16a5a[PII_REDACTED]63deeb028854fd23a`

### Details
Root cause:

- `src/apm_cli/deps/plugin_parser.py:336-348`
  - `_resolve_sources()` joins manifest-controlled `agents`, `skills`, `commands`, and directory-form `hooks` paths with `plugin_path`
  - it checks only `exists()` and `is_symlink()`
  - it does not resolve the candidate and verify containment inside the plugin root
- `src/apm_cli/deps/plugin_parser.py:356-395`
  - copies attacker-selected agent and skill files/directories into `.apm/`
- `src/apm_cli/deps/plugin_parser.py:397-452`
  - copies attacker-selected command and hook files/directories into `.apm/`
- `src/apm_cli/deps/plugin_parser.py:436-442`
  - string-form hook config paths are also copied without a root-containment check

There is already a safer precedent in the same module:

- `src/apm_cli/deps/plugin_parser.py:195-210`
  - `_read_mcp_file()` resolves the candidate path
  - rejects paths escaping the plugin root
  - rejects symlinks

Reachability:

- Local install path:
  - `src/apm_cli/commands/install.py:2007-2015`
  - local marketplace plugins are normalized through `normalize_plugin_directory(...)`
- Remote install path:
  - `src/apm_cli/deps/github_downloader.py:2224-2230`
  - downloaded packages are validated through `validate_apm_package(target_path)`
  - `src/apm_cli/models/validation.py:164-172`, `224-226`, `304-324`
  - marketplace plugins are normalized through the same vulnerable path after clone

Project write-back path:

- `src/apm_cli/integration/prompt_integrator.py:38-56`
  - reads `.apm/prompts/*.prompt.md`
- `src/apm_cli/integration/prompt_integrator.py:170-189`
  - writes prompt files into `.github/prompts/`
- `src/apm_cli/commands/install.py:2496-2514`
  - auto-integrates package primitives after install

This means a malicious dependency can cause APM to read from outside the dependency itself and materialize host-local content into managed install output and, in the verified prompt case, directly into the victim project.

### PoC
The attached zip contains a comple