---
name: evolution_sort_injection_validator
description: "Evolution registers must validate sortby/sortdir via safeOrderBy/validateSortby — raw ORDER BY $_REQUEST[sortby] is SQLi"
metadata: 
  node_type: memory
  type: project
  originSessionId: 0d352521-6edd-44ce-a241-1b14619c30f5
---

Evolution registries historically built `ORDER BY $_REQUEST[sortby] $_REQUEST[sortdir]` from raw request input — a repo-wide ORDER BY SQL-injection pattern. The `$sortfields` whitelist only existed client-side (registrysortcol.inc / `data-sort-field` table headers); there was no server-side check.

Fix shipped in `functions.php`: `validateSortby($allowed, $default)` (whitelists the column, returns default on miss), `safeSortdir()` (forces asc/desc), and `safeOrderBy($allowed, $default)` (wrapper returning `"col dir"`).

**Why:** `sortby` arrives as a column-name string from the page's `$sortfields`; ORDER BY injection is exploitable (boolean/subquery extraction).

**How to apply:**
- Simple sink → `$orderBy = safeOrderBy([...whitelist...], 'id'); ... ORDER BY $orderBy`.
- Pages that post-process sortby (str_replace alias expansion, statustext→IF) → call `$_REQUEST['sortby'] = validateSortby([...], 'id'); $_REQUEST['sortdir'] = safeSortdir();` BEFORE the existing transforms, then leave them. The whitelist mirrors the RAW `data-sort-field` values (incl. shorthand like `q.id`, `t(c(...))`).
- Whitelist source of truth = that register's `$sortfields` array or `data-sort-field` attrs.

Fixed across ~21 sinks in: lead/site/dispatch/defect/picking/backorder (merge batch) + client/staff/supportvideos/mfreq/bill/invedit/invoiceadd/quotedit/purchaseadd/req/jobedit/addressbook/asset/stocktake. stocktakesave also had raw `page`/`stocktakeid`/`sort-order` — parameterized.

Now guarded too: `plugins/shopify/plugin.php` (3 ORDER BY sinks ~L3489/5936/8944 — inventory/jobregister/clientlist whitelists, Group B). Follow-up pass parameterized the full WHERE clauses of the three search registers there: converted the two `getFieldArray` sinks (job register, contacts) to `DB::SELECT(...,null,0,PDO::FETCH_ASSOC)` with `?` placeholders (built `$contactParams`/`$pmParams`/`$retentionParams`/`$clientParams` in query order), parameterized the inventory desc-search + category `IN()`, the custommeta sub-query, `getDispatchedJobItems`, and four `$search_string` LIKE sinks (getEvoProducts/getEvoCats/B2B customers/fetchCategoryRegistry). Note: `getFieldArray`/`selectQuery` in functions.php still have NO bind support — must use `DB::SELECT` to parameterize. Shared include `invspecsearchsection.inc` (used by core inventory.inc too) hardened: `$invsid`→(int), `$invsval`→addslashes, `$invscond`→whitelisted AND/OR. Note: the `foreach($data as $k=>$v) $$k=$v;` extraction pattern recurs ~40× in the plugin — makes locals request-derived, so audit by SQL sink not by var name. See [[portal_minified_no_build]] is unrelated; this is evolution repo per [[workspace_repos]].
