Human
Machine
REPRO-2026-00144 CRITICAL SQLi
Verified
phpMyFAQ: unauthenticated SQL injection via User-Agent header in captcha API
thorsten/phpmyfaq (composer) May 22, 2026
What's the vulnerability?
phpMyFAQ is a PHP FAQ / knowledge-base application. Its BuiltinCaptcha
class builds SQL by interpolating the unsanitized User-Agent HTTP header
directly into queries:
BuiltinCaptcha::garbageCollector()— into aDELETEqueryBuiltinCaptcha::saveCaptcha()— into anINSERTquery
The public, unauthenticated endpoint GET /api/captcha reaches this code
path. An attacker can therefore perform unauthenticated time-based blind SQL
injection simply by sending a request with a crafted User-Agent header — no
login, no token, no user interaction required.
Root Cause Analysis
# 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.
One Command
Verify with pruva-verify
Run the Pruva CLI to automatically fetch and execute the reproduction script.
pruva-verify REPRO-2026-00144 or
pruva-verify GHSA-289f-fq7w-6q2w or
pruva-verify CVE-2026-46364 Install:
curl -fsSL https://pruva.dev/install.sh | sh Or Run Manually
1
Download the script
curl -O https://pruva.dev/api/v1/reproductions/REPRO-2026-00144/artifacts/reproduction_steps.sh 2
Make executable
chmod +x reproduction_steps.sh 3
Run the script
./reproduction_steps.sh Run in a VM, container, or disposable environment. This exploits a real vulnerability.
How Pruva Reproduced This
Watch the AI agent's step-by-step process.
Loading session...
Artifacts
No artifacts available