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 a DELETE query
  • BuiltinCaptcha::saveCaptcha() — into an INSERT query

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