CVE-2026-34729: phpMyFAQ: Stored XSS via Regex Bypass in Filter::removeAttributes()
Summary The sanitization pipeline for FAQ content is: 1. Filter::filterVar($input, FILTERSANITIZESPECIALCHARS) — encodes <, >, ", ', & to HTML entities 2. htmlentitydecode($input, ENTQUOTES | ENTHTML5) — decodes entities back to characters 3. Filter::removeAttributes($input) — removes dangerous HTML attributes
The removeAttributes() regex at line 174 only matches attributes with double-quoted values: php pregmatchall(pattern: '/[a-z]+=".+"/iU', subject: $html, matches: $attributes);
This regex does NOT match: - Attributes with single quotes: onerror='alert(1)' - Attributes without quotes: onerror=alert(1)
An attacker can bypass sanitization by submitting FAQ content with unquoted or single-quoted event handler attributes.
Details
Affected File: phpmyfaq/src/phpMyFAQ/Filter.php, line 174
Sanitization flow for FAQ question field:
FaqController::create() lines 110, 145-149: php $question = Filter::filterVar($data->question, FILTERSANITIZESPECIALCHARS); // ... ->setQuestion(Filter::removeAttributes(htmlentitydecode( (string) $question, ENTQUOTES | ENTHTML5, encoding: 'UTF-8', )))
Template rendering: faq.twig line 36: twig <h2 class="mb-4 border-bottom">{{ question | raw }}</h2>
How the bypass works:
1. Attacker submits: <img src=x onerror=alert(1)> 2. After FILTERSANITIZESPECIALCHARS: <img src=x onerror=alert(1)> 3. After htmlentitydecode(): <img src=x onerror=alert(1)> 4. pregmatchall('/[a-z]+=".+"/iU', ...) runs: - The regex requires ="..." (double quotes) - onerror=alert(1) has NO quotes → NOT matched - src=x has NO quotes → NOT matched - No attributes are found for removal 5. Output: <img src=x onerror=alert(1)> (XSS payload intact) 6. Template renders with |raw: JavaScript executes in browser
Why double-quoted attributes are (partially) protected:
For <img src="x" onerror="alert(1)">: - The regex matches both src="x" and onerror="alert(1)" - src is in $keep → preserved - onerror is NOT in $keep → removed via strreplace() - Output: <img src="x"> (safe)
But this protection breaks with single quotes or no quotes.
PoC
Step 1: Create FAQ with XSS payload (requires authenticated admin): bash curl -X POST 'https://target.example.com/admin/api/faq/create' \ -H 'Content-Type: application/json' \ -H 'Cookie: PHPSESSID=adminsession' \ -d '{ "data": { "pmf-csrf-token": "validcsrftoken", "question": "<img src=x onerror=alert(document.cookie)>", "answer": "Test answer", "lang": "en", "categories[]": 1, "active": "yes", "tags": "test", "keywords": "test", "author": "test", "email": "test@test.com" } }'
Step 2: XSS triggers on public FAQ page
Any user (including unauthenticated visitors) viewing the FAQ page triggers the XSS: https://target.example.com/content/{categoryId}/{faqId}/{lang}/{slug}.html
The FAQ title is rendered with |raw in faq.twig line 36 without HtmlSanitizer processing (the processQuestion() method in FaqDisplayService only applies search highlighting, not cleanUpContent()).
Alternative payloads: html <img/src=x onerror=alert(1)> <svg onload=alert(1)> <details open ontoggle=alert(1)>
Impact
- Public XSS: The XSS executes for ALL users viewing the FAQ page, not just admins. - Session hijacking: Steal session cookies of all users viewing the FAQ. - Phishing: Display fake login forms to steal credentials. - Worm propagation: Self-replicating XSS that creates new FAQs with the same payload. - Malware distribution: Redirect users to malicious sites.
Note: While planting the payload requires admin access, the XSS executes for all visitors (public-facing). This is not self-XSS.
Other sources
phpMyFAQ is an open source FAQ web application. Prior to version 4.1.1, there is a stored XSS vulnerability via Regex Bypass in Filter::removeAttributes(). This issue has been patched in version 4.1.1.
— NVD