{"schema":"threatline.record/2","record":{"id":"GHSA-fg92-v5rr-4xjq","aliases":["GHSA-fg92-v5rr-4xjq"],"summary":"Stored XSS via Over-Permissive HTML Sanitizer Allowing `<iframe src=\"data:text/html,...\">`","details":"Summary :\r\n\r\nphpMyFAQ's HTML sanitization configuration allows both the `<iframe>` element and the `data:` URI scheme.\r\n\r\nBecause `data:` URLs do not contain a hostname, the configured media-host allowlist does not restrict them. As a result, an attacker with permission to submit or create rich-text FAQ/News content may store an iframe whose `src` contains a `data:text/html` document with embedded JavaScript.\r\n\r\nThe malicious content survives sanitization and is later rendered using Twig's `|raw` filter, causing arbitrary JavaScript execution inside the embedded iframe when a victim views the affected FAQ or News page.\r\n\r\nSeverity :\r\n\r\nMedium\r\n\r\nCVSS 3.1:\r\n\r\n`CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N`\r\n\r\nScore: **6.1 (Medium)**\r\n\r\nCWE:\r\n\r\n* CWE-79 — Improper Neutralization of Input During Web Page Generation\r\n* CWE-80 — Improper Neutralization of Script-Related HTML Tags\r\n\r\nConfidence:\r\n\r\n**High**\r\n\r\nThe issue was reproduced at runtime against the real `symfony/html-sanitizer` configuration used by phpMyFAQ.\r\n\r\nAffected Version :\r\n\r\nphpMyFAQ **4.2.x**\r\n\r\nConfirmed on:\r\n\r\n`phpMyFAQ 4.2.0-beta`\r\n\r\nReviewed Git commit:\r\n\r\n`0fb7ecd`\r\n\r\nAffected Component :\r\n\r\nFAQ and News rich-text content rendering.\r\n\r\nAffected files include:\r\n\r\n`phpmyfaq/src/phpMyFAQ/Helper/FaqHelper.php`\r\n\r\n`phpmyfaq/assets/templates/default/faq.twig`\r\n\r\n`phpmyfaq/assets/templates/default/news.twig`\r\n\r\nRelevant entry points include:\r\n\r\nFAQ creation API\r\n\r\n`POST /api/.../faqs`\r\n\r\nand the Administration News API.\r\n\r\nDescription :\r\n\r\nphpMyFAQ uses Symfony's HTML Sanitizer to clean rich-text content before displaying it.\r\n\r\nThe sanitizer configuration explicitly permits the `<iframe>` element:\r\n\r\n```php\r\n->allowElement(\r\n    'iframe',\r\n    ['title', 'src', 'width', 'height', 'allow', 'allowfullscreen']\r\n)\r\n```\r\n\r\nAt the same time, the following media schemes are allowed:\r\n\r\n```php\r\n->allowMediaSchemes([\r\n    'https',\r\n    'http',\r\n    'mailto',\r\n    'data'\r\n])\r\n```\r\n\r\nand link schemes include:\r\n\r\n```php\r\n->allowLinkSchemes([\r\n    'https',\r\n    'http',\r\n    'mailto',\r\n    'data'\r\n])\r\n```\r\n\r\nThe combination of:\r\n\r\n```text\r\niframe\r\n+\r\ndata:\r\n```\r\n\r\nis unsafe.\r\n\r\nAn attacker can supply content such as:\r\n\r\n```html\r\n<iframe src=\"data:text/html,<script>alert(document.domain)</script>\"></iframe>\r\n```\r\n\r\nThe sanitizer produces output similar to:\r\n\r\n```html\r\n<iframe src=\"data:text/html,&lt;script&gt;alert(document.domain)&lt;/script&gt;\"></iframe>\r\n```\r\n\r\nAlthough the `<script>` characters are HTML-encoded inside the attribute, the browser decodes the attribute value when navigating the iframe.\r\n\r\nThe resulting iframe therefore loads:\r\n\r\n```text\r\ndata:text/html,<script>alert(document.domain)</script>\r\n```\r\n\r\nand executes the JavaScript.\r\n\r\nThe host allowlist does not prevent this because `data:` URLs are hostless.\r\n\r\nThe sanitized content is later rendered into templates using unescaped output:\r\n\r\n```twig\r\n{{ answer|raw }}\r\n```\r\n\r\nfor FAQ content and:\r\n\r\n```twig\r\n{{ mainPageContent|raw }}\r\n```\r\n\r\nfor News content.\r\n\r\nThis converts the sanitizer weakness into stored script execution.\r\n\r\nProof of Concept :\r\n\r\nA minimal payload is:\r\n\r\n```html\r\n<iframe src=\"data:text/html,<script>alert(document.domain)</script>\"></iframe>\r\n```\r\n\r\nThe audit reproduced the sanitizer behavior with the exact phpMyFAQ configuration.\r\n\r\nSanitizer input:\r\n\r\n```html\r\n<iframe src=\"data:text/html,<script>alert(document.domain)</script>\"></iframe>\r\n```\r\n\r\nObserved sanitizer output:\r\n\r\n```html\r\n<iframe src=\"data:text/html,&lt;script&gt;alert(document.domain)&lt;/script&gt;\"></iframe>\r\n```\r\n\r\nWhen this markup is interpreted by the browser, the iframe navigates to a `data:text/html` document and executes the embedded JavaScript.\r\n\r\nOther common payloads such as:\r\n\r\n```html\r\n<script>alert(1)</script>\r\n```\r\n\r\n```html\r\n<svg onload=alert(1)>\r\n```\r\n\r\nand:\r\n\r\n```html\r\n<img src=x onerror=alert(1)>\r\n```\r\n\r\nwere stripped by the sanitizer.\r\n\r\nAn external iframe URL such as:\r\n\r\n```html\r\n<iframe src=\"https://evil.example/\"></iframe>\r\n```\r\n\r\nalso loses its `src` when the hostname is not in the configured allowlist.\r\n\r\nThis demonstrates that the vulnerability specifically results from the interaction between the permitted iframe element and the allowed hostless `data:` scheme.\r\n\r\nSteps To Reproduce :\r\n\r\n1. Deploy phpMyFAQ 4.2.x in a controlled test environment.\r\n\r\n2. Ensure that FAQ rich-text submission is enabled.\r\n\r\nFor the frontend FAQ flow, this can apply when:\r\n\r\n```text\r\nmain.enableWysiwygEditorFrontend = true\r\n```\r\n\r\nand the current account is permitted to create FAQ content.\r\n\r\n3. Submit an FAQ answer containing:\r\n\r\n```html\r\n<iframe src=\"data:text/html,<script>alert(document.domain)</script>\"></iframe>\r\n```\r\n\r\n4. Save the FAQ.\r\n\r\n5. Open the public page that renders the affected FAQ.\r\n\r\n6. The FAQ content is processed through:\r\n\r\n```text\r\nFaqHelper::cleanUpContent()\r\n```\r\n\r\n7. Observe that the `<iframe>` element remains present and its `data:` URL is preserved.\r\n\r\n8. The content is rendered in:\r\n\r\n```text\r\nfaq.twig\r\n```\r\n\r\nusing:\r\n\r\n```twig\r\n{{ answer|raw }}\r\n```\r\n\r\n9. The victim's browser loads the `data:text/html` document inside the iframe.\r\n\r\n10. The embedded JavaScript executes.\r\n\r\nThe same sanitizer configuration also affects News content rendered through:\r\n\r\n```twig\r\n{{ mainPageContent|raw }}\r\n```\r\n\r\nImpact :\r\n\r\nAn attacker who can inject rich-text FAQ or News content can store arbitrary JavaScript that executes when another user views the affected public page.\r\n\r\nPotential consequences include:\r\n\r\n**Phishing UI**\r\n\r\nThe injected iframe can display arbitrary HTML and JavaScript-controlled interfaces designed to imitate the legitimate application.\r\n\r\n**Top-Frame Redirect**\r\n\r\nJavaScript executing in the iframe may attempt to redirect the top-level page to an attacker-controlled destination.\r\n\r\n**External Resource Loading**\r\n\r\nThe iframe can load attacker-controlled resources, enabling tracking or interaction with external infrastructure.\r\n\r\n**Persistent Execution**\r\n\r\nBecause the payload is stored as FAQ or News content, it can affect multiple users until the malicious content is removed.\r\n\r\nThe impact is lower than traditional same-origin stored XSS.\r\n\r\nThe script executes inside a `data:` iframe with an opaque origin.\r\n\r\nTherefore it cannot directly access:\r\n\r\n```text\r\nphpMyFAQ session cookies\r\nsame-origin DOM\r\nCSRF tokens from the parent page\r\n```\r\n\r\nunder normal browser origin isolation rules.\r\n\r\nNevertheless, attacker-controlled JavaScript execution remains possible on a trusted phpMyFAQ page and can be used for phishing or navigation-based attacks.\r\n\r\nRoot Causes :\r\n\r\nThe vulnerability is caused by an unsafe sanitizer policy.\r\n\r\nThe configuration simultaneously allows:\r\n\r\n```text\r\niframe\r\n```\r\n\r\nand:\r\n\r\n```text\r\ndata:\r\n```\r\n\r\nmedia URLs.\r\n\r\nThe `data:` scheme is likely intended to support inline images, but it is allowed generically rather than being restricted to safe MIME types such as:\r\n\r\n```text\r\ndata:image/png\r\ndata:image/jpeg\r\ndata:image/gif\r\ndata:image/webp\r\n```\r\n\r\nBecause `data:` URLs do not have a hostname, the following host restriction:\r\n\r\n```php\r\n->allowMediaHosts($allowedHosts)\r\n```\r\n\r\ndoes not constrain them.\r\n\r\nThe resulting sanitized content is then passed into Twig templates using:\r\n\r\n```twig\r\n|raw\r\n```\r\n\r\nwhich relies entirely on the sanitizer to remove dangerous content.\r\n\r\nTherefore the security failure is caused by the combination of:\r\n\r\n1. `<iframe>` being permitted.\r\n2. `data:` being permitted in media schemes.\r\n3. Host validation not applying to hostless `data:` URLs.\r\n4. Sanitized rich-text content being rendered with `|raw`.\r\n\r\nSuggested Fix :\r\n\r\nRemove the `data:` scheme from allowed media and link schemes.\r\n\r\nFor example:\r\n\r\n```php\r\n->allowElement(\r\n    'iframe',\r\n    ['title', 'src', 'width', 'height', 'allow', 'allowfullscreen']\r\n)\r\n\r\n->allowMediaSchemes([\r\n    'https',\r\n    'http',\r\n    'mailto'\r\n])\r\n\r\n->allowMediaHosts($allowedHosts)\r\n\r\n->allowLinkSchemes([\r\n    'https',\r\n    'http',\r\n    'mailto'\r\n]);\r\n```\r\n\r\nIf inline images are required, do not enable generic `data:` URLs.\r\n\r\nInstead, validate them separately and allow only explicitly supported image MIME types, for example:\r\n\r\n```text\r\n^data:image/(png|jpeg|gif|webp);base64,\r\n```\r\n\r\nA `data:` URI containing:\r\n\r\n```text\r\ntext/html\r\n```\r\n\r\nmust never be accepted as the source of an iframe.\r\n\r\nAdditional hardening should include:\r\n\r\n* Add a `sandbox` attribute to allowed iframes.\r\n* Restrict iframe sources to a strict allowlist of trusted hosts.\r\n* Deploy a strong Content-Security-Policy.\r\n* Restrict `frame-src`.\r\n* Avoid `unsafe-inline` in `script-src`.\r\n* Add regression tests for dangerous `data:` iframe payloads.\r\n\r\nA suitable regression test should verify that:\r\n\r\n```html\r\n<iframe src=\"data:text/html,<script>1</script>\"></iframe>\r\n```\r\n\r\nis either completely removed or has its `src` attribute removed.\r\n\r\nAt the same time, a legitimate trusted embed such as:\r\n\r\n```html\r\n<iframe src=\"https://www.youtube.com/embed/example\"></iframe>\r\n```\r\n\r\nmay remain allowed when its host is explicitly trusted.\r\n\r\nReferences :\r\n\r\nAffected source locations:\r\n\r\n* `phpmyfaq/src/phpMyFAQ/Helper/FaqHelper.php`\r\n* `phpmyfaq/assets/templates/default/faq.twig`\r\n* `phpmyfaq/assets/templates/default/news.twig`\r\n* `phpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.php`\r\n* `phpmyfaq/src/phpMyFAQ/Controller/Administration/Api/NewsController.php`\r\n\r\nRelevant locations:\r\n\r\n`FaqHelper.php:134-137`\r\n\r\n`faq.twig:38`\r\n\r\n`news.twig:9`\r\n\r\n`Frontend/Api/FaqController.php:123`\r\n\r\n`Administration/Api/NewsController.php:178,192`\r\n\r\nWeakness classifications:\r\n\r\n* CWE-79 — Cross-Site Scripting\r\n* CWE-80 — Improper Neutralization of Script-Related HTML Tags\r\n\r\nRelevant technology:\r\n\r\n* Symfony HTML Sanitizer\r\n* Twig raw rendering\r\n* `data:` URI scheme\r\n* HTML `<iframe>` embedding\r\n","categories":["Unclassified"],"product":"phpMyFAQ","vendor":"","ecosystem":null,"affected_ranges":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-fg92-v5rr-4xjq","source_modified_at":"2026-10-03T11:33:05Z","fetched_at":"2026-10-25T21:40:15.971031Z","package":{"ecosystem":"php","name":"phpMyFAQ"},"range":" <= 4.1.8"}],"fixed_versions":[{"source":"GitHub Advisory Database","source_url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-fg92-v5rr-4xjq","source_modified_at":"2026-10-03T11:33:05Z","fetched_at":"2026-10-25T21:40:15.971031Z","package":{"ecosystem":"php","name":"phpMyFAQ"},"version":"4.1.9"}],"cvss":5.4,"severity":"Medium","kev":{"listed":false},"epss":{"probability":null,"percentile":null,"score_date":null},"published_at":"2026-10-03T11:33:05Z","modified_at":"2026-10-03T11:33:05Z","provenance":[{"source":"GitHub Advisory Database","url":"https://github.com/thorsten/phpMyFAQ/security/advisories/GHSA-fg92-v5rr-4xjq","fetched_at":"2026-10-25T21:40:15.971031Z","source_modified_at":"2026-10-03T11:33:05Z"}],"sources":["GitHub Advisory Database"],"references":[],"locations":[],"motoko_assessment":{"status":"requires_architecture_review","automatic_remediation":false},"classification":{"method":"product-first software-layer rules","rules_version":"2.1","matched_on":null,"confidence":"unknown","reviewed":false},"change_timestamp":"2026-10-03T11:33:05Z","content_hash":"e121c5988bc45c332e8c65500d89c76c0636203190a88c41087cf7e69e902ed1"}}
