Skip to content

harden(plugin): enforce plugin-root containment for manifest paths#760

Merged
danielmeppiel merged 5 commits intomainfrom
harden/plugin-manifest-path-containment
Apr 19, 2026
Merged

harden(plugin): enforce plugin-root containment for manifest paths#760
danielmeppiel merged 5 commits intomainfrom
harden/plugin-manifest-path-containment

Conversation

@danielmeppiel
Copy link
Copy Markdown
Collaborator

Summary

Tightens _map_plugin_artifacts so every manifest-controlled component path (agents, skills, commands, hooks) is verified to resolve inside the plugin root before it's copied into .apm/. Today the helper only checks exists() / is_symlink(); if a custom path points outside the plugin directory it gets copied anyway, which is inconsistent with the trust boundary we already enforce for MCP config files (_read_mcp_file) and with the rest of the codebase's use of ensure_path_within.

Changes

  • src/apm_cli/deps/plugin_parser.py: introduce _is_within_plugin helper that wraps ensure_path_within with a warning-and-skip behaviour, and apply it inside _resolve_sources (list + string forms) and the hooks string-config branch.
  • tests/unit/test_plugin_parser.py: new TestPathTraversalProtection class covering absolute paths, .. traversal, and the hooks string-config branch, plus a positive case to confirm legitimate in-root paths still work.
  • CHANGELOG.md: hardening note under Fixed.

Validation

  • uv run pytest tests/unit/test_plugin_parser.py -> 67 passed (61 existing + 6 new)
  • uv run pytest tests/unit tests/test_console.py -> 3939 passed

Notes

Behaviour change is conservative: only paths escaping the plugin root are dropped, and a warning is logged identifying the offending component. No public API changes.

Marketplace plugin normalization (_map_plugin_artifacts) accepts
custom component paths from plugin.json for agents, skills, commands,
and hooks. Previously the resolver only checked existence and
symlink status, so a manifest entry pointing outside the plugin
directory would still be copied into .apm/.

Route every manifest-controlled path through ensure_path_within(),
matching the existing pattern in _read_mcp_file. Entries that escape
the plugin root are skipped with a warning; in-root paths continue to
work unchanged.

Adds regression coverage for absolute paths, traversal sequences in
both list and string forms, and the hooks string-config branch.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot AI review requested due to automatic review settings April 19, 2026 06:11
Comment thread src/apm_cli/deps/plugin_parser.py Fixed
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR hardens marketplace/Claude plugin normalization by ensuring manifest-specified artifact paths cannot escape the plugin root before being copied into .apm/, aligning plugin handling with existing path-containment practices (ensure_path_within).

Changes:

  • Add _is_within_plugin() helper and apply containment checks to manifest-provided agents/skills/commands paths and the hooks string-config file path branch.
  • Add unit tests covering absolute paths, .. traversal, hooks string-config traversal, and a positive in-root case.
  • Add a changelog entry under Fixed for the hardening.
Show a summary per file
File Description
src/apm_cli/deps/plugin_parser.py Introduces containment guard for manifest-controlled component paths during plugin artifact mapping.
tests/unit/test_plugin_parser.py Adds regression tests ensuring escaped paths are rejected and legitimate in-root paths still work.
CHANGELOG.md Documents the security hardening under the Unreleased Fixed section.

Copilot's findings

  • Files reviewed: 3/3 changed files
  • Comments generated: 2

Comment thread CHANGELOG.md Outdated
Comment thread src/apm_cli/deps/plugin_parser.py Outdated
danielmeppiel and others added 2 commits April 19, 2026 08:27
- Apply containment + symlink check to default component branch in
  _resolve_sources so a plugin shipping 'agents'/'skills'/'commands'/
  'hooks' as a symlink to an external target is rejected without
  needing a manifest override. Add regression test.
- Demote the rejected-path detail to debug logging and keep the
  user-facing warning generic, addressing the CodeQL clear-text
  logging finding on manifest-controlled values.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Comment thread src/apm_cli/deps/plugin_parser.py Fixed
danielmeppiel and others added 2 commits April 19, 2026 08:40
CodeQL py/clear-text-logging-sensitive-data flagged manifest-controlled
path strings flowing into _logger calls. Drop the rejected raw value
and exception from logging entirely; the component name alone is
enough to identify the rejected manifest field.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@danielmeppiel danielmeppiel merged commit a03f47f into main Apr 19, 2026
5 checks passed
@danielmeppiel danielmeppiel deleted the harden/plugin-manifest-path-containment branch April 19, 2026 06:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants