fix(api): harden plugin manager OCI install path (H-3)#121
Merged
Conversation
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 |
5 tasks
Restores 'Python — Lint & Type-check' to green by satisfying the repo-wide ruff lint + format gates that run across all services in CI.
This was referenced May 14, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Hardens
services/api/app/services/plugin_manager.PluginManager.install_from_ociagainst 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 pullargv is now a Python list (no shell) and ordered asoras pull --output <tmp> -- <ref>so--outputis 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_symlinkswalks the extracted tree and refuses any symlinks;_safe_copytreewrapsshutil.copytreewithsymlinks=Falseand an explicit per-file recheck. Prevents arbitrary file overwrite via crafted OCI layers._select_extracted_plugin_dirpicks the plugin root by looking forplugin.yaml/plugin.json, instead of trusting "single top-level dir". Images without a manifest are rejected.AISOC_PLUGINS_DIR. InPLUGIN_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._validate_plugin_idaccept/reject matrix (.., NUL, separators, empty, length cap)._validate_oci_refaccept/reject matrix incl. metadata-host refs._assert_no_symlinkson nested symlinks._select_extracted_plugin_dirfor flat / single-subdir / no-manifest layouts._safe_copytreerejects symlinks at copy time.install_from_ocihappy path, strict-mode unsigned rejection, signature-before-copy ordering, argv shape (--outputbefore--, ref as sole positional after--), and rejection of metadata-host refs.oras pullagainst 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.md—PLUGIN_TRUST_MODEandPLUGIN_TRUSTED_KEYS_DIRnow appear in the API service reference and link back to the new security section.Refs: H-3
Made with Cursor