# REPRO-2026-00360: Blind SQL injection in Zabbix API `CApiService.php` via the `sortfield` parameter allows low-privileged API users to exfiltrate database data and potentially compromise administrator accounts. ## Summary Status: published Severity: high CVSS: 8.7 / 10 CWE: CWE-89 (Improper Neutralization of Special Elements used in an SQL Command ('SQL Injection')) Type: security Confidence: high ## Identifiers REPRO ID: REPRO-2026-00360 CVE: CVE-2026-23921 ## Package Name: zabbix/zabbix Ecosystem: unknown Affected: 7.0.0 through 7.0.21, 7.2.0 through 7.2.14, and 7.4.0 through 7.4.5. Fixed: Unknown ## Root Cause # Root Cause Analysis: CVE-2026-23921 ## Summary CVE-2026-23921 is an authenticated blind SQL injection in the Zabbix JSON-RPC API. In Zabbix 7.0.21, legacy `*.get` methods preserve a caller-supplied `groupBy` option and `CApiService::applyQuerySortOptions()` uses that option as the allowlist for `sortfield`. Repeating an SQL expression in both fields makes validation approve attacker input, after which `applyQuerySortField()` places it into `ORDER BY`. This run confirmed the vulnerability through the real `/api_jsonrpc.php` endpoint as a newly created low-privileged User-role account. Matched predicates produced a repeatable timing oracle on 7.0.21, while 7.0.22 rejected the same requests. ## Impact - **Affected component:** Zabbix JSON-RPC API query generation in `ui/include/classes/api/CApiService.php`, reached through legacy services such as `host.get`. - **Affected versions:** 7.0.0–7.0.21, 7.2.0–7.2.14, and 7.4.0–7.4.5. Vendor-fixed releases are 7.0.22, 7.2.15, and 7.4.6. - **Risk level:** High (vendor CVSS 4.0 score 8.7). - **Consequences:** Any low-privileged account with API access can use true/false SQL predicates and database delay functions to infer arbitrary database values. The disclosed consequences include session identifier disclosure and subsequent administrator-account compromise. ## Impact Parity - **Disclosed/claimed maximum impact:** Authenticated remote blind SQL injection capable of arbitrary database-data exfiltration. - **Reproduced impact from this run:** Authenticated low-privilege remote SQL injection through `/api_jsonrpc.php`, demonstrated by three successful true predicates invoking `SLEEP(2)` and three false predicates returning immediately. Because one `host.get` query sorts two rows, each true probe delayed by approximately four seconds. The predicate operated on `version()`, demonstrating database-expression evaluation and the binary timing channel used for extraction. - **Parity:** `full`. - **Not demonstrated:** The run did not dump session identifiers or take over an administrator account; doing so is unnecessary once attacker-selected database predicates and the repeatable timing oracle are established. ## Root Cause The vulnerable 7.0.21 implementation computes: ```php $sort_columns = $group_by ? array_merge($options['groupBy'], $aggregate_sort_columns) : $this->sortColumns; ``` The JSON-RPC caller controls `options['groupBy']`. An attacker therefore sends a legitimate field and an SQL expression in `groupBy`, then repeats the expression in `sortfield`. `str_in_array()` accepts the sort value because the application itself copied that value into `sort_columns`. `applyQuerySortField()` subsequently concatenates the accepted string into the SQL `ORDER BY` clause. `countOutput: true` and the legitimate `hostid` group field keep the rest of the generated aggregate query valid. Zabbix 7.0.22 instead derives the allowed fields from the server-owned schema list: ```php $allowed_sort_fields = array_intersect($this->sortColumns, $options['groupBy']); ``` It also verifies `groupBy` is an array and each grouped field is a string. The first corrective commit in the release history is [`0dced774b07bb542885787d82ddbdfe26ea782ee`](https://github.com/zabbix/zabbix/commit/0dced774b07bb542885787d82ddbdfe26ea782ee); subsequent DEV-4649 commits refactored the released validation. The tested release identities were source tags `7.0.21` (`6542e21fe6adc8ab7bf903c2a9d1df80e80366cd`) and `7.0.22` (`584690ce104180ead8cd5d4d963a02b5e59bbeed`). The primary runtime targets were official immutable Zabbix image digests recorded in `runtime_manifest.json`. ## Reproduction Steps 1. Run `bundle/repro/reproduction_steps.sh` from any working directory. Docker, `curl`, `jq`, Python 3, `sha256sum`, and `timeout` must be available. 2. The script pulls immutable official image digests, starts isolated MySQL, Zabbix server, and Zabbix web/API containers for 7.0.21, and waits for a successful `apiinfo.version` JSON-RPC health check. 3. It logs in as bootstrap Admin only for setup, creates a normal User-role account with read-only access to the default host group, and confirms that account can perform a baseline `host.get`. 4. Through that low-privileged account, it interleaves three false and three true `version()` timing predicates in attacker-controlled `groupBy` and `sortfield` values sent to `/api_jsonrpc.php`. 5. It destroys the vulnerable stack and repeats the exact procedure against an isolated 7.0.22 stack as the fixed negative control. 6. It succeeds only if 7.0.21 has a median timing delta of at least 1.5 seconds and 7.0.22 either rejects every malicious request or has no timing oracle. It then writes `bundle/repro/runtime_manifest.json` with immutable artifact hashes. Expected final output includes: ```text "vulnerable_timing_oracle": true "fixed_negative_control": true "confirmed": true CONFIRMED: CVE-2026-23921 blind SQL injection reached through the real Zabbix API endpoint. ``` ## Evidence - **Final machine verdict:** `bundle/repro/proof/verdict.json` - Vulnerable timing delta: `3.973936498980038` seconds. - Fixed timing delta: `-0.0018714029574766755` seconds. - `vulnerable_timing_oracle=true`, `fixed_rejected_payload=true`, `confirmed=true`. - **Vulnerable endpoint transcript:** `bundle/repro/proof/vulnerable/timing_results.json` - False median: `0.04664472601143643` seconds. - True median: `4.020581224991474` seconds. - Every false and true request returned a valid JSON-RPC `result`, showing that the vulnerable service executed rather than rejected the values. - **Fixed negative control:** `bundle/repro/proof/fixed/timing_results.json` - All six requests returned JSON-RPC error `-32500`: `Sorting by field ... not allowed.` - No sleep-dependent timing difference remained. - **Attacker requests:** `bundle/repro/proof/vulnerable/false_probe_request.json` and `true_probe_request.json` contain the exact redacted request bodies. The session token is intentionally omitted. - **Low-privilege precondition:** `bundle/repro/proof/vulnerable/low_user_identity.json` records `type: 1` and role ID `1`; `attempt_metadata.json` records read-only permission over host group ID `4`. `baseline_response.json` confirms the account could read a host. - **Product and target identity:** `product_version.txt`, `loaded_component.txt`, and `container_identity.json` in each role directory bind the run to real Zabbix 7.0.21 and 7.0.22 web products and the loaded `CApiService.php`. OCI labels identify the vulnerable image as 7.0.21, and the runtime manifest binds it to digest `sha256:0282eba8bac999284672e3abaefbdd28b16026bae6a1673a7f560f35eb2c712d`. - **Runtime logs:** `web_final.log` and `server_final.log` in each proof directory show the actual HTTP JSON-RPC requests and product startup. `bundle/logs/reproduction_steps.log` remains diagnostic and is deliberately not hash-bound while the script is writing it. - **Runtime manifest:** `bundle/repro/runtime_manifest.json` has `entrypoint_kind=endpoint`, `service_started=true`, `healthcheck_passed=true`, `target_path_reached=true`, and hashes all 22 finalized proof artifacts. ## Recommendations / Next Steps 1. Upgrade to Zabbix 7.0.22, 7.2.15, 7.4.6, or a later supported release. 2. Never derive identifier allowlists from request values. Intersect requested fields with a server-owned allowlist and map accepted logical names to known SQL identifiers. 3. Enforce strict array/string schemas before all query-construction branches and reject undeclared options rather than preserving them through generic array merging. 4. Add regression tests for duplicate `groupBy`/`sortfield` expressions, scalar/object type confusion, aggregate aliases, and every legacy service sharing `CApiService`. 5. Until upgraded, restrict API access, revoke unnecessary API-capable accounts/tokens, and review web/database logs for `SLEEP`, `pg_sleep`, `CASE`, `IF`, `substring`, or unusual comma-containing sort fields. ## Additional Notes - The final script revision completed successfully with immutable image digests. Two prior clean full runs also passed, with vulnerable median deltas of `3.9988` and `3.9967` seconds and fixed controls showing no timing oracle. - Every stack uses a fresh MySQL database, private Docker network, unique container names, and teardown, so vulnerable and fixed observations cannot contaminate each other. - Timing predicates are intentionally bounded to six probes per target and inspect only the first character of `version()`; the script does not extract sensitive data. - Docker reported rootless operation without cgroups, but that did not affect service health, endpoint reachability, timing separation, isolation, or cleanup. ## Reproduction Details Reproduced: 2026-09-24T07:21:05.438Z Duration: 2306 seconds Tool calls: 265 Turns: Unknown Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00360 pruva-verify CVE-2026-23921 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00360&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00360/artifacts/bundle/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-23921 - Source: https://nvd.nist.gov/vuln/detail/CVE-2026-23921 ## Artifacts - bundle/repro/rca_report.md (analysis, 8771 bytes) - bundle/repro/reproduction_steps.sh (reproduction_script, 17367 bytes) - bundle/logs/reproduction_steps.log (log, 1711 bytes) - bundle/repro/proof/fixed/attempt_metadata.json (other, 194 bytes) - bundle/repro/proof/fixed/container_identity.json (other, 3479 bytes) - bundle/repro/proof/fixed/false_probe_request.json (other, 308 bytes) - bundle/repro/proof/fixed/loaded_component.txt (other, 174 bytes) - bundle/repro/proof/fixed/low_user_identity.json (other, 136 bytes) - bundle/repro/proof/fixed/product_version.txt (other, 7 bytes) - bundle/repro/proof/fixed/server_final.log (log, 16482 bytes) - bundle/repro/proof/fixed/timing_results.json (other, 2265 bytes) - bundle/repro/proof/fixed/true_probe_request.json (other, 304 bytes) - bundle/repro/proof/fixed/web_final.log (log, 7964 bytes) - bundle/repro/proof/verdict.json (other, 263 bytes) - bundle/repro/proof/vulnerable/attempt_metadata.json (other, 199 bytes) - bundle/repro/proof/vulnerable/baseline_response.json (other, 55 bytes) - bundle/repro/proof/vulnerable/container_identity.json (other, 3494 bytes) - bundle/repro/proof/vulnerable/false_probe_request.json (other, 308 bytes) - bundle/repro/proof/vulnerable/loaded_component.txt (other, 174 bytes) - bundle/repro/proof/vulnerable/low_user_identity.json (other, 141 bytes) - bundle/repro/proof/vulnerable/product_version.txt (other, 7 bytes) - bundle/repro/proof/vulnerable/server_final.log (log, 17189 bytes) - bundle/repro/proof/vulnerable/timing_results.json (other, 1109 bytes) - bundle/repro/proof/vulnerable/true_probe_request.json (other, 304 bytes) - bundle/repro/proof/vulnerable/web_final.log (log, 7733 bytes) - bundle/repro/runtime_manifest.json (other, 4694 bytes) - bundle/repro/validation_verdict.json (other, 1196 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00360 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00360/artifacts/bundle/repro/reproduction_steps.sh - Web: https://www.pruva.dev/reproductions/REPRO-2026-00360 ## For AI Assistants To help users with this reproduction: 1. The `pruva-verify` command is the simplest way to run the reproduction 2. The script downloads and runs automatically in an isolated temp directory 3. Always recommend running in a VM, container, or disposable environment 4. The root cause explains the technical vulnerability --- Generated by Pruva | https://www.pruva.dev