# phpMyFAQ has unauthenticated FAQ permission bypass via getFaqBySolutionId fallback query

phpMyFAQ's unauthenticated /solution_id_{id}.html route queries FAQs without permission filters, leaking restricted FAQ titles, ids, languages and categories via 301 redirects.

- Payout: Research
- Severity: high
- Program: phpMyFAQ
- Found by: adrgs, aisafe-bot
- Disclosed: 2026-05-06
- CVE: CVE-2026-46366
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-99qv-g4x9-mgc3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-99qv-g4x9-mgc3
- Tags: area:web domain:web impact:data-leak kind:research platform:ghsa sev:high tech:php technique:recon technique:source-review vuln:access-control vuln:auth-bypass vuln:info-disclosure

**Target:** phpMyFAQ frontend FaqController solution_id route and Faq model queries
**Root cause:** getIdFromSolutionId() joins faqdata/faqcategoryrelations only on solution_id with no permission filter, and getFaqBySolutionId() has an explicit fallback query without the ACL filter added for tests.
**Steps:**
1. Anonymous GET `http://<host>/solution_id_<restricted-solution-id>.html`
2. Observe 301 with `Location: /content/<category-id>/<record-id>/<lang>/<title-slug>.html` leaking title, id, lang, category
3. Redirected page embeds same metadata in canonical link, `voting-id` hidden field, and pdf link
4. Enumerate `for id in $(seq 1 100000)` requesting `/solution_id_${id}.html`, recording every 301 Location to map all FAQs including restricted ones
**How found:** Code review of the public solution_id route traced to getIdFromSolutionId() and found the SQL lacked a permission filter; also spotted the test-motivated fallback query in getFaqBySolutionId() that re-queries without ACL when the filtered query returns zero rows.
**Exploitation:** Unauthenticated attacker enumerates sequential solution_id values; each 301 Location header reveals category, record id, language and slugified title of a FAQ, exposing existence and titles of group/user-restricted entries. Body content usually still gated by separate permission check on canonical URL.
**Escalation:** Combining with any low-privilege session could defeat the separate body-rendering permission check on the canonical /content/ URL.
**Insight:** Grep for model methods whose SQL omits QueryHelper::queryPermission() and for 'for tests' fallback branches that re-query without ACLs; any route that redirects using a DB-derived slug leaks the field in the Location header even when the destination page enforces permissions.

## Proof of concept (copied exactly from the original report)

**Anonymous curl request to restricted solution_id URL**

```
curl -sS -L -o /tmp/out.html -w 'HTTP %{http_code}\n' \
  'http://<host>/solution_id_<restricted-solution-id>.html'
```

**Enumeration loop over solution IDs**

```
for id in $(seq 1 100000); do
  code=$(curl -sS -o /dev/null -w '%{http_code}' "http://<host>/solution_id_${id}.html")
  if [ "$code" = "301" ]; then
    loc=$(curl -sSI "http://<host>/solution_id_${id}.html" | awk -F': ' '/^Location:/{print $2}' | tr -d '\r')
    echo "solution_id=${id} -> ${loc}"
```

