Thanks to visit codestin.com
Credit goes to github.com

Skip to content

fix(api): harden plugin manager OCI install path (H-3)#121

Merged
beenuar merged 2 commits into
mainfrom
fix/plugin-manager-hardening
May 14, 2026
Merged

fix(api): harden plugin manager OCI install path (H-3)#121
beenuar merged 2 commits into
mainfrom
fix/plugin-manager-hardening

Conversation

@beenuar
Copy link
Copy Markdown
Owner

@beenuar beenuar commented May 14, 2026

Summary

Hardens services/api/app/services/plugin_manager.PluginManager.install_from_oci against multiple plugin-supply-chain attacks.

  • _validate_plugin_id — strict ^[a-z0-9_.-]+$ regex; rejects .., NUL, /, \, empty, and over-long IDs before any path is constructed from them.
  • _validate_oci_ref — conservative ref regex plus a deny-list of cloud metadata host substrings (169.254.169.254, metadata.google.internal, metadata.azure.com, …). Defense-in-depth against SSRF via plugin installs.
  • oras pull argv is now a Python list (no shell) and ordered as oras pull --output <tmp> -- <ref> so --output is parsed as a flag pair and the ref is the sole positional after --. Prevents argv injection via the ref while keeping the call functionally correct.
  • _assert_no_symlinks walks the extracted tree and refuses any symlinks; _safe_copytree wraps shutil.copytree with symlinks=False and an explicit per-file recheck. Prevents arbitrary file overwrite via crafted OCI layers.
  • _select_extracted_plugin_dir picks the plugin root by looking for plugin.yaml/plugin.json, instead of trusting "single top-level dir". Images without a manifest are rejected.
  • Signature verification now happens before files are copied into AISOC_PLUGINS_DIR. In PLUGIN_TRUST_MODE=strict, an unsigned or invalid plugin is rejected without ever touching the live plugins dir — eliminating the verify-after-write race.

Test plan

  • services/api && python -m pytest tests/test_plugin_manager.py -q — 106 passed.
  • New unit tests cover:
    • _validate_plugin_id accept/reject matrix (.., NUL, separators, empty, length cap).
    • _validate_oci_ref accept/reject matrix incl. metadata-host refs.
    • _assert_no_symlinks on nested symlinks.
    • _select_extracted_plugin_dir for flat / single-subdir / no-manifest layouts.
    • _safe_copytree rejects symlinks at copy time.
    • install_from_oci happy path, strict-mode unsigned rejection, signature-before-copy ordering, argv shape (--output before --, ref as sole positional after --), and rejection of metadata-host refs.
  • Manual smoke: oras pull against a benign signed test image in a staging tenant (out of scope for this PR; flagged for the reviewer).

Docs

  • apps/docs/docs/operations/security.md — new "OCI install hardening (H-3)" section under Plugin trust.
  • apps/docs/docs/deployment/env-vars.mdPLUGIN_TRUST_MODE and PLUGIN_TRUSTED_KEYS_DIR now appear in the API service reference and link back to the new security section.

Refs: H-3

Made with Cursor

Multiple defense-in-depth fixes for the OCI install flow in
`services/api/app/services/plugin_manager.py`:

* Validate every `plugin_id` against a strict `^[a-z0-9_.-]+$` regex
  before it is ever used to construct an on-disk path. Prevents path
  traversal via `..`, NUL bytes, or absolute paths in manifest IDs.
* Validate OCI references with a conservative regex and refuse refs
  whose registry host substrings match cloud-metadata endpoints
  (`169.254.169.254`, `metadata.google.internal`, `metadata.azure.com`,
  etc.). Defense in depth against SSRF via plugin installs.
* Build the `oras pull` argv as a Python list (no shell) and place
  `--output <path>` before `--` so flags are parsed correctly while
  still preventing argv injection via the ref.
* After pull, walk the extracted tree and reject any symlinks before
  copying. Use a `_safe_copytree()` wrapper around `shutil.copytree`
  with `symlinks=False` and explicit per-file copies that re-check
  for symlinks at copy time. Prevents arbitrary file overwrite via
  malicious OCI layers.
* New `_select_extracted_plugin_dir()` helper picks the plugin root
  by looking for `plugin.yaml`/`plugin.json` rather than assuming a
  single top-level directory; rejects images with no manifest.
* Verify the plugin signature against the trusted keys directory
  *before* copying files into `AISOC_PLUGINS_DIR`. In `strict` trust
  mode, an unsigned or invalid plugin is rejected without ever
  touching the live plugins dir, eliminating the
  signature-verification-after-write race.

Tests
-----
106 new/updated tests in `services/api/tests/test_plugin_manager.py`
covering:
* `_validate_plugin_id` accept/reject matrix incl. `..`, NUL, `/`, `\`,
  empty, length cap.
* `_validate_oci_ref` accept/reject matrix incl. metadata hosts and
  malformed refs.
* `_assert_no_symlinks` detects symlinks anywhere in the tree.
* `_select_extracted_plugin_dir` for flat, single-subdir, and
  no-manifest layouts.
* `_safe_copytree` rejects symlinks at copy time.
* `install_from_oci` happy path, strict-mode unsigned rejection,
  signature-before-copy ordering, argv shape (`--output` before `--`,
  ref as sole positional after `--`), and rejection of metadata-host
  refs.

Docs
----
* `apps/docs/docs/operations/security.md` — new "OCI install hardening
  (H-3)" section under "Plugin trust" documenting each check and the
  operator-visible implications.
* `apps/docs/docs/deployment/env-vars.md` — added `PLUGIN_TRUST_MODE`
  and `PLUGIN_TRUSTED_KEYS_DIR` to the API service reference, with a
  link to the new security section.

Refs: H-3
def _stub_oras_pull(monkeypatch, fake):
"""Replace ``subprocess.run`` *inside* the plugin_manager module so
``install_from_oci`` exercises our fake without touching the real CLI."""
import app.services.plugin_manager as pm # noqa: PLC0415
Restores 'Python — Lint & Type-check' to green by satisfying the
repo-wide ruff lint + format gates that run across all services in CI.
@beenuar beenuar marked this pull request as ready for review May 14, 2026 10:59
@beenuar beenuar merged commit f550a65 into main May 14, 2026
25 checks passed
@beenuar beenuar deleted the fix/plugin-manager-hardening branch May 14, 2026 10:59
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.

2 participants