# phpMyFAQ has Stored XSS in Admin FAQ Editor via HTML Entity Bypass in Frontend FAQ Submission

phpMyFAQ decodes HTML entities after strip_tags in guest FAQ submission, storing executable HTML that fires stored XSS in the admin editor and enables admin account takeover.

- Payout: Research
- Severity: high
- Program: phpMyFAQ
- Found by: JosanGeorge
- Disclosed: 2026-09-24
- CVE: CVE-2026-56736
- Write-up quality: 7/10
- Original: https://github.com/advisories/GHSA-pgwp-vc7q-cvj3
- Library page: https://bugbountylibrary.com/r/ghsa%3AGHSA-pgwp-vc7q-cvj3
- Tags: area:web domain:web impact:ato impact:integrity kind:research platform:ghsa sev:high tech:php technique:chained technique:source-review vuln:xss

**Target:** PHP web app FAQ submission API and admin FAQ editor template
**Root cause:** FILTER_SANITIZE_SPECIAL_CHARS/strip_tags removes real tags but leaves HTML entities; html_entity_decode() then converts them into executable HTML before DB storage, and the admin Twig template renders content with |raw.
**Steps:**
1. Ensure main.enableWysiwygEditorFrontend=true and records.allowNewFaqsForGuests=true (default), with a FAQ category existing.
2. POST to `/api/faq/create` with answer `</textarea><img src=x onerror=alert(document.domain)><textarea>` and valid/empty captcha.
3. strip_tags passes the entity-encoded string unchanged; html_entity_decode converts it to `</textarea><img src=x onerror=alert(document.domain)><textarea>` and it is stored raw.
4. Admin opens `GET /admin/faq/edit/{faqId}/{lang}`; the |raw textarea render breaks out and onerror fires in admin context.
**How found:** Reporter noticed strip_tags only strips real tag syntax, not entities, and that html_entity_decode runs afterward when the frontend WYSIWYG option is enabled; then traced the stored value to the admin template's |raw sink while the public view was sanitized.
**Exploitation:** Unauthenticated guest submits a crafted FAQ answer; when an admin reviews/edits the entry the payload executes in the admin browser, allowing session cookie theft, admin account creation, or config changes.
**Escalation:** Stored XSS in admin context escalates to full admin account takeover and system configuration modification.
**Insight:** When a sanitizer runs before an entity-decode (or any decode/normalize) step, entity-encoded tags survive and become live HTML; check admin-only render paths separately since public views may use a real sanitizer while admin templates use |raw.

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

**curl POST injecting entity-encoded XSS payload**

```
curl -X POST https://TARGET/api/faq/create \
  -H 'Content-Type: application/json' \
  -d '{
    "name": "Legitimate User",
    "email": "user@example.com",
    "question": "How to configure SMTP settings?",
    "answer": "&lt;/textarea&gt;&lt;img src=x onerror=alert(document.domain)&gt;&lt;textarea&gt;",
    "lang": "en",
    "keywords": "smtp email",
    "rubrik": ["1"],
    "captcha": "<valid-captcha-or-empty-if-disabled>"
  }'
```

