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.11main commit 70b34faa16a5a783424698163deeb028854fd23aRoot 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_pathexists() and is_symlink()src/apm_cli/deps/plugin_parser.py:356-395
.apm/src/apm_cli/deps/plugin_parser.py:397-452
.apm/src/apm_cli/deps/plugin_parser.py:436-442
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 pathReachability:
src/apm_cli/commands/install.py:2007-2015normalize_plugin_directory(...)src/apm_cli/deps/github_downloader.py:2224-2230validate_apm_package(target_path)src/apm_cli/models/validation.py:164-172, 224-226, 304-324Project write-back path:
src/apm_cli/integration/prompt_integrator.py:38-56
.apm/prompts/*.prompt.mdsrc/apm_cli/integration/prompt_integrator.py:170-189
.github/prompts/src/apm_cli/commands/install.py:2496-2514
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.
The attached zip contains a complete maintainer-ready proof-of-concept package, including runnable scripts, payload templates, captured output, and the exact validation environment.
Primary end-to-end apm install reproduction:
apm-cli 0.8.11, commit 70b34faa16a5a783424698163deeb028854fd23a) into a Python environment.victim\secret.md
with content:
# STOLEN VIA APM INSTALL
plugin.json:{
"name": "evil-plugin",
"commands": "D:\\absolute\\path\\to\\victim\\secret.md"
}
apm.yml that references the malicious plugin.apm install
.github/prompts/secret.prompt.md
# STOLEN VIA APM INSTALL
Verified console output from the included PoC:
[>] Installing dependencies from apm.yml...
[+] ./evil-plugin (local)
|-- 1 prompts integrated -> .github/prompts/
[*] Installed 1 APM dependency.
PoC succeeded.
Integrated into project: ...\.github\prompts\secret.prompt.md
Integrated content:
# STOLEN VIA APM INSTALL
Secondary remote-parity reproduction:
reproduce-remote-parity.py exercises GitHubPackageDownloader.download_package(...) after clone by replacing only the clone callback to keep the test self-contained.<download-target>/.apm/prompts/secret.prompt.md
This is a path traversal / arbitrary local file copy issue in the package install flow.
Who is impacted:
apm install against a malicious or compromised plugin dependencyWhat an attacker gains:
.apm/ during install.apm/.apm/prompts/Practical impact:
.github/prompts/, increasing the chance that sensitive or attacker-selected content is committed, synced, or consumed by other toolingRecommended fix:
plugin_path.resolve().agents, skills, commands, and both hooks code paths.commandscommands../ traversal in agents../ traversal in skills../ traversal in hooks