# REPRO-2026-00144: phpMyFAQ: unauthenticated SQL injection via User-Agent header in captcha API ## Summary Status: published Severity: critical Type: security Confidence: Unknown ## Identifiers REPRO ID: REPRO-2026-00144 GHSA: GHSA-289f-fq7w-6q2w CVE: CVE-2026-46364 ## Package Name: thorsten/phpmyfaq Ecosystem: composer Affected: < 4.1.2 Fixed: 4.1.2 ## Root Cause # RCA Report: CVE-2026-46364 ## Summary phpMyFAQ versions prior to 4.1.2 contain an unauthenticated SQL injection vulnerability in the `BuiltinCaptcha` class. The `User-Agent` HTTP header is interpolated directly into SQL `DELETE` and `INSERT` queries in `garbageCollector()` and `saveCaptcha()` without any escaping or parameterization. An attacker can exploit this by sending a crafted `User-Agent` header to the `/api/captcha` endpoint, causing arbitrary SQL to execute in the context of the application's database connection. ## Impact - **Package/component affected**: `thorsten/phpmyfaq` — `phpMyFAQ\Captcha\BuiltinCaptcha` - **Affected versions**: `< 4.1.2` (vulnerable); `4.1.2` and later (fixed) - **Risk level**: Critical (CVSS 3.1 base 9.8) - **Consequences**: Unauthenticated time-based blind SQL injection via the public `GET /api/captcha` endpoint. An attacker can read, modify, or delete database contents, bypass authentication, or execute administrative actions depending on the database privileges of the application user. ## Root Cause In `BuiltinCaptcha::garbageCollector()` and `BuiltinCaptcha::saveCaptcha()` (phpMyFAQ 4.1.1), the `$this->userAgent`, `$this->ip`, `$this->code`, and language values are concatenated directly into SQL strings using `sprintf()`: ```php $delete = sprintf( "DELETE FROM %sfaqcaptcha WHERE useragent = '%s' AND language = '%s' AND ip = '%s'", Database::getTablePrefix(), $this->userAgent, $this->configuration->getLanguage()->getLanguage(), $this->ip, ); ``` Because the `User-Agent` value is controlled by the HTTP client and is not escaped before interpolation, an attacker can inject SQL syntax (e.g., `' OR '1'='1`) that alters the query's semantics. The fix commit (`545bdffb11244a4741cd29ac909a849c9a6a2e53` in the 4.1.2 release timeline) introduces an `escapeQueryValue()` helper that calls `$this->configuration->getDb()->escape()` on each untrusted value before concatenation, effectively neutralizing the injection vector. ## Reproduction Steps 1. Run `repro/reproduction_steps.sh` 2. The script clones the phpMyFAQ repository, checks out the vulnerable tag `4.1.1`, installs dependencies via Composer, and starts a built-in PHP server (`php -S`) hosting a minimal captcha-generating endpoint. 3. The script sends an HTTP `GET` request to `http://localhost:8766/` with a malicious `User-Agent: test' OR '1'='1` header. 4. The captcha endpoint instantiates `BuiltinCaptcha` from the actual phpMyFAQ source code and calls `getCaptchaImage()`, which internally triggers `garbageCollector()` → `saveCaptcha()`. 5. The script inspects the SQLite `faqcaptcha` table to verify the payload was executed as SQL rather than stored as a literal string. 6. The script repeats steps 2–5 against the fixed tag `4.1.2` to confirm the payload is now escaped and stored literally. ### Expected Evidence - **Vulnerable (4.1.1)**: The `useragent` column in the SQLite database contains `1` (the boolean result of the injected SQL expression `'1'='1'`), proving the payload was evaluated as SQL. - **Fixed (4.1.2)**: The `useragent` column contains the literal string `test' OR '1'='1`, proving the input was escaped before reaching the query. ## Evidence ### Log file - `logs/repro.log` ### Key excerpts ``` === Testing vulnerable version 4.1.1 === Database useragent value: 1 CONFIRMED: 4.1.1 is VULNERABLE (User-Agent payload executed as SQL, boolean result stored) === Testing fixed version 4.1.2 === Database useragent value: test' OR '1'='1 CONFIRMED: 4.1.2 is FIXED (User-Agent stored as literal string) ``` ### Runtime manifest - `repro/runtime_manifest.json` captures structured evidence: - `vulnerable_useragent_value`: `"1"` - `fixed_useragent_value`: `"test' OR '1'='1"` - `payload`: `"test' OR '1'='1"` - `target_url`: `http://localhost:8766/` - `http_status`: `200` ## Recommendations / Next Steps 1. **Upgrade immediately** to phpMyFAQ 4.1.2 or later. The patch adds escaping via `$this->configuration->getDb()->escape()` for all untrusted values injected into captcha SQL queries. 2. **Review other query-building code** in the codebase for similar unsanitized `sprintf()` patterns, especially in `Faq.php`, `Relation.php`, `Search.php`, `Tags.php`, and `SearchDatabase.php`, which were also refactored in the 4.1.2 release. 3. **Add parameterized query enforcement** as a code-quality rule (e.g., via static analysis or custom linters) to prevent raw string concatenation with user input in SQL generation. 4. **Regression testing**: The existing `BuiltinCaptchaTest` in the project now includes `testSaveCaptchaEscapesUserAgentAndIpValues()` and `testGarbageCollectorEscapesUserAgentAndIpValues()`, which should be run in CI for every release. ## Additional Notes - **Idempotency**: `repro/reproduction_steps.sh` was executed twice consecutively on a clean environment; both runs produced identical results, confirming the script is idempotent. - **Edge cases / limitations**: The reproduction uses SQLite because it is lightweight and requires no external database server. The injection vector is database-agnostic; MySQL/MariaDB users would observe similar behavior (with `SLEEP()`-based time delays as the primary signal). PostgreSQL and SQL Server backends are also affected because the root cause is unsanitized string interpolation, not a dialect-specific feature. - **No authentication required**: The reproduction hits an unauthenticated endpoint, matching the advisory's assessment that no login, token, or user interaction is needed to exploit this vulnerability. ## Reproduction Details Reproduced: 2026-05-22T10:56:40.275Z Duration: 3735 seconds Tool calls: 328 Turns: 298 Handoffs: 2 ## Quick Verification Run one of these commands to verify locally: pruva-verify REPRO-2026-00144 pruva-verify GHSA-289f-fq7w-6q2w pruva-verify CVE-2026-46364 Or open in GitHub Codespaces (zero-friction, auto-runs): https://github.com/codespaces/new?ref=repro/REPRO-2026-00144&repo=N3mes1s/pruva-sandbox Or download and run the script manually: curl -O https://api.pruva.dev/v1/reproductions/REPRO-2026-00144/artifacts/repro/reproduction_steps.sh chmod +x reproduction_steps.sh ./reproduction_steps.sh WARNING: Run in a sandboxed environment. This exploits a real vulnerability. ## References - GitHub Advisory: https://github.com/advisories/GHSA-289f-fq7w-6q2w - NVD: https://nvd.nist.gov/vuln/detail/CVE-2026-46364 - Source: https://github.com/thorsten/phpMyFAQ ## Artifacts - repro/rca_report.md (analysis, 5591 bytes) - repro/reproduction_steps.sh (reproduction_script, 6270 bytes) - vuln_variant/rca_report.md (analysis, 6695 bytes) - vuln_variant/reproduction_steps.sh (reproduction_script, 438 bytes) - bundle/context.json (other, 2896 bytes) - bundle/metadata.json (other, 736 bytes) - bundle/ticket.md (ticket, 3712 bytes) - repro/runtime_manifest.json (other, 659 bytes) - repro/validation_verdict.json (other, 2275 bytes) - vuln_variant/root_cause_equivalence.json (other, 1137 bytes) - vuln_variant/patch_analysis.md (documentation, 4059 bytes) - vuln_variant/variant_manifest.json (other, 2659 bytes) - vuln_variant/runtime_manifest.json (other, 1161 bytes) - vuln_variant/validation_verdict.json (other, 1987 bytes) - vuln_variant/source_identity.json (other, 600 bytes) - vuln_variant/variant_test.php (other, 9203 bytes) - logs/vuln_variant/variant.log (log, 1212 bytes) - logs/repro.log (log, 371 bytes) ## API Access - JSON: https://api.pruva.dev/v1/reproductions/REPRO-2026-00144 - Script: https://api.pruva.dev/v1/reproductions/REPRO-2026-00144/artifacts/repro/reproduction_steps.sh - Web: https://pruva.dev/r/REPRO-2026-00144 ## 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://pruva.dev