phpMyFAQ: Stored XSS via Over-Permissive HTML Sanitizer Allowing `<iframe src="data:text/html,...">`
What fixes it
- 4.1.9phpMyFAQ, phpGitHub
A fixed version on one release line is not the upgrade target for every line. Check your exact version and any vendor backports.
Description
Summary :
phpMyFAQ's HTML sanitization configuration allows both the <iframe> element and the data: URI scheme.
Because 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.
The 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.
Severity :
Medium
CVSS 3.1:
CVSS:3.1/AV:N/AC:L/PR:L/UI:R/S:C/C:L/I:L/A:N
Score: 6.1 (Medium)
CWE:
- CWE-79 — Improper Neutralization of Input During Web Page Generation
- CWE-80 — Improper Neutralization of Script-Related HTML Tags
Confidence:
High
The issue was reproduced at runtime against the real symfony/html-sanitizer configuration used by phpMyFAQ.
Affected Version :
phpMyFAQ 4.2.x
Confirmed on:
phpMyFAQ 4.2.0-beta
Reviewed Git commit:
0fb7ecd
Affected Component :
FAQ and News rich-text content rendering.
Affected files include:
phpmyfaq/src/phpMyFAQ/Helper/FaqHelper.php
phpmyfaq/assets/templates/default/faq.twig
phpmyfaq/assets/templates/default/news.twig
Relevant entry points include:
FAQ creation API
POST /api/.../faqs
and the Administration News API.
Description :
phpMyFAQ uses Symfony's HTML Sanitizer to clean rich-text content before displaying it.
The sanitizer configuration explicitly permits the <iframe> element:
->allowElement(
'iframe',
['title', 'src', 'width', 'height', 'allow', 'allowfullscreen']
)At the same time, the following media schemes are allowed:
->allowMediaSchemes([
'https',
'http',
'mailto',
'data'
])and link schemes include:
->allowLinkSchemes([
'https',
'http',
'mailto',
'data'
])The combination of:
iframe + data:
is unsafe.
An attacker can supply content such as:
<iframe src="data:text/html,<script>alert(document.domain)</script>"></iframe>
The sanitizer produces output similar to:
<iframe src="data:text/html,<script>alert(document.domain)</script>"></iframe>
Although the <script> characters are HTML-encoded inside the attribute, the browser decodes the attribute value when navigating the iframe.
The resulting iframe therefore loads:
data:text/html,<script>alert(document.domain)</script>
and executes the JavaScript.
The host allowlist does not prevent this because data: URLs are hostless.
The sanitized content is later rendered into templates using unescaped output:
{{ answer|raw }}for FAQ content and:
{{ mainPageContent|raw }}for News content.
This converts the sanitizer weakness into stored script execution.
Proof of Concept :
A minimal payload is:
<iframe src="data:text/html,<script>alert(document.domain)</script>"></iframe>
The audit reproduced the sanitizer behavior with the exact phpMyFAQ configuration.
Sanitizer input:
<iframe src="data:text/html,<script>alert(document.domain)</script>"></iframe>
Observed sanitizer output:
<iframe src="data:text/html,<script>alert(document.domain)</script>"></iframe>
When this markup is interpreted by the browser, the iframe navigates to a data:text/html document and executes the embedded JavaScript.
Other common payloads such as:
<script>alert(1)</script>
<svg onload=alert(1)>
and:
<img src=x onerror=alert(1)>
were stripped by the sanitizer.
An external iframe URL such as:
<iframe src="https://evil.example/"></iframe>
also loses its src when the hostname is not in the configured allowlist.
This demonstrates that the vulnerability specifically results from the interaction between the permitted iframe element and the allowed hostless data: scheme.
Steps To Reproduce :
- Deploy phpMyFAQ 4.2.x in a controlled test environment.
- Ensure that FAQ rich-text submission is enabled.
For the frontend FAQ flow, this can apply when:
main.enableWysiwygEditorFrontend = true
and the current account is permitted to create FAQ content.
- Submit an FAQ answer containing:
<iframe src="data:text/html,<script>alert(document.domain)</script>"></iframe>
- Save the FAQ.
- Open the public page that renders the affected FAQ.
- The FAQ content is processed through:
FaqHelper::cleanUpContent()
- Observe that the
<iframe>element remains present and itsdata:URL is preserved.
- The content is rendered in:
faq.twig
using:
{{ answer|raw }}- The victim's browser loads the
data:text/htmldocument inside the iframe.
- The embedded JavaScript executes.
The same sanitizer configuration also affects News content rendered through:
{{ mainPageContent|raw }}Impact :
An attacker who can inject rich-text FAQ or News content can store arbitrary JavaScript that executes when another user views the affected public page.
Potential consequences include:
Phishing UI
The injected iframe can display arbitrary HTML and JavaScript-controlled interfaces designed to imitate the legitimate application.
Top-Frame Redirect
JavaScript executing in the iframe may attempt to redirect the top-level page to an attacker-controlled destination.
External Resource Loading
The iframe can load attacker-controlled resources, enabling tracking or interaction with external infrastructure.
Persistent Execution
Because the payload is stored as FAQ or News content, it can affect multiple users until the malicious content is removed.
The impact is lower than traditional same-origin stored XSS.
The script executes inside a data: iframe with an opaque origin.
Therefore it cannot directly access:
phpMyFAQ session cookies same-origin DOM CSRF tokens from the parent page
under normal browser origin isolation rules.
Nevertheless, attacker-controlled JavaScript execution remains possible on a trusted phpMyFAQ page and can be used for phishing or navigation-based attacks.
Root Causes :
The vulnerability is caused by an unsafe sanitizer policy.
The configuration simultaneously allows:
iframe
and:
data:
media URLs.
The data: scheme is likely intended to support inline images, but it is allowed generically rather than being restricted to safe MIME types such as:
data:image/png data:image/jpeg data:image/gif data:image/webp
Because data: URLs do not have a hostname, the following host restriction:
->allowMediaHosts($allowedHosts)
does not constrain them.
The resulting sanitized content is then passed into Twig templates using:
|raw
which relies entirely on the sanitizer to remove dangerous content.
Therefore the security failure is caused by the combination of:
<iframe>being permitted.data:being permitted in media schemes.- Host validation not applying to hostless
data:URLs. - Sanitized rich-text content being rendered with
|raw.
Suggested Fix :
Remove the data: scheme from allowed media and link schemes.
For example:
->allowElement(
'iframe',
['title', 'src', 'width', 'height', 'allow', 'allowfullscreen']
)
->allowMediaSchemes([
'https',
'http',
'mailto'
])
->allowMediaHosts($allowedHosts)
->allowLinkSchemes([
'https',
'http',
'mailto'
]);If inline images are required, do not enable generic data: URLs.
Instead, validate them separately and allow only explicitly supported image MIME types, for example:
^data:image/(png|jpeg|gif|webp);base64,
A data: URI containing:
text/html
must never be accepted as the source of an iframe.
Additional hardening should include:
- Add a
sandboxattribute to allowed iframes. - Restrict iframe sources to a strict allowlist of trusted hosts.
- Deploy a strong Content-Security-Policy.
- Restrict
frame-src. - Avoid
unsafe-inlineinscript-src. - Add regression tests for dangerous
data:iframe payloads.
A suitable regression test should verify that:
<iframe src="data:text/html,<script>1</script>"></iframe>
is either completely removed or has its src attribute removed.
At the same time, a legitimate trusted embed such as:
<iframe src="https://www.youtube.com/embed/example"></iframe>
may remain allowed when its host is explicitly trusted.
References :
Affected source locations:
phpmyfaq/src/phpMyFAQ/Helper/FaqHelper.phpphpmyfaq/assets/templates/default/faq.twigphpmyfaq/assets/templates/default/news.twigphpmyfaq/src/phpMyFAQ/Controller/Frontend/Api/FaqController.phpphpmyfaq/src/phpMyFAQ/Controller/Administration/Api/NewsController.php
Relevant locations:
FaqHelper.php:134-137
faq.twig:38
news.twig:9
Frontend/Api/FaqController.php:123
Administration/Api/NewsController.php:178,192
Weakness classifications:
- CWE-79 — Cross-Site Scripting
- CWE-80 — Improper Neutralization of Script-Related HTML Tags
Relevant technology:
- Symfony HTML Sanitizer
- Twig raw rendering
data:URI scheme- HTML
<iframe>embedding
Text from GitHub Advisory Database
Affected versions 1
- phpMyFAQ, php <= 4.1.8GitHub