---
name: plugin-security-review
description: Plugin system audit+modernization roadmap (merged doc); status, open items, canonical M1-M6 phasing
metadata: 
  node_type: memory
  type: project
  originSessionId: 34e55f34-7301-4172-9626-5a54dac397f9
  modified: 2026-07-28T13:25:32.191Z
---

Evolution's plugin system was reviewed (scored **4/10**). The audit doc is
**`evolution/audits/PLUGIN_SECURITY_REVIEW.md`** — read it first. As of 2026-07-28 it is now the
**single source of truth**: the separate modernization plan (was `/config/.claude/plans/we-have-a-plug-in-vast-stallman.md`,
since DELETED from disk) was **merged into it** as Part B (§11–§15). §11 reconciles the colliding
"Phase" numbering — **use the canonical build phases M1–M6 going forward**, not the old review-phase 1–4.

**Status (verified against live code + dev DB 2026-07-28):**
- **M1 (=old Phase 1) SHIPPED to `staging` + `production`** (commit `bfa9919ff`); `plugin-review` branch is 0 ahead of staging (retired). Closed: stored-XSS on settings page (new `plugins/block_sanitiser.php` — save-time HTML/CSS/URL allow-list; HTML fields render RAW by design so control is save-time sanitisation not render escaping), two SQLi (table-source + `gettablefields` → `information_schema` whitelist), missing admin AuthZ gate on `plugin_ajax.php`, `LIKE '%id%'`→`FIND_IN_SET` tenant leak (5 sites), Shopify settings-hijack (F6). **O1 (WHERE-clause SQLi) FIXED 2026-06-19** via `evo_validate_block_where()`. See [[evolution_ajax_save_auth_gate]], [[evolution_sort_injection_validator]].
- Migration `plugins_manifest_columns.sql` = **APPLIED** (cols live on `evolution.plugins`, unread — loader is M3).

**Still OPEN (all unchanged since 2026-06-19):**
- ⚠️ **`customextrameta_drop_company.sql` NOT RUN — OVERDUE**: F7 code (stopped writing `company`) is in prod but the `company varchar(10) NOT NULL` col is still live; non-fatal only because prod `sql_mode` is non-strict (rows get `''`, 806 already). Strict mode → error 1364. Top deploy action.
- **R1** = no CSRF token on `plugin_ajax.php` mutations (top remaining code fix).
- **O3** = sanitiser save-time only; pre-Phase-1 `blocks` rows need one-off backfill.
- **O2** = raw `name`/`id` attrs + reflected `$_REQUEST['id']` at `settings.php:48` (permanently killed by M4's escaped `BlockRenderer`).
- **R2/R3/R4** = import mime-sniff / unquoted `Content-Disposition` / `validate-api.php` internal gate (R4 folds into M6 `PluginBase`).

**Roadmap (Part B, §12):** M2 migration framework (`plugin_migrations` table + `SqlStatementSplitter.php`), M3 `plugin.json` manifest+loader+shim (`PluginManifest.php`; cols already shipped), M4 hook/event registry de-`eval` (`PluginEvents.php`) + typed settings + `BlockRenderer.php`, M5 lifecycle + `plugin_company` join table (kills comma-list matching), M6 durability (`PluginBase`, test harness, Composer). Compat contract: preserve `$_SESSION['companyplugins'][n]` shape; keep all 16 plugins working via synthesis shim. Posture ~5/10, arch ≈4/10. Only M1 executed.
