{"id":"GHSA-3h6j-9x8m-rg3g","aliases":[],"title":"Graby has stored XSS via iframe srcdoc Attribute in htmLawed Sanitization Config","description":"## Summary\n\nGraby's `cleanupXss()` function configures htmLawed with conflicting settings: `safe=1` (which removes `<iframe>`) combined with `'elements' => '*+iframe-meta'` (which re-enables `<iframe>`). htmLawed does not sanitize the `srcdoc` attribute, allowing injection of arbitrary JavaScript that executes when the content is rendered via `|raw` in templates.\n\n## Root Cause\n\n**`src/Graby.php` lines 1038-1048:**\n```php\nhtmLawed($html, [\n    'safe' => 1,                    // removes <iframe>\n    'elements' => '*+iframe-meta',  // re-adds <iframe>, overrides safe=1\n    'deny_attribute' => 'style',    // srcdoc is NOT denied\n]);\n```\n\nThe `safe=1` and `+iframe` combination is a conflict: `safe` mode is designed to strip dangerous elements, but the elements override re-enables `<iframe>` without also blocking the dangerous `srcdoc` attribute.\n\n## Proof of Concept\n\nInput to `cleanupXss()`:\n```html\n<iframe srcdoc=\"&lt;script&gt;alert(document.domain)&lt;/script&gt;\"></iframe>\n```\n\nOutput (unchanged — htmLawed passes it through):\n```html\n<iframe srcdoc=\"&lt;script&gt;alert(document.domain)&lt;/script&gt;\"></iframe>\n```\n\nWhen rendered via `{{ content|raw }}` in a template, `srcdoc` executes in an `about:srcdoc` frame with the same origin as the page. **Confirmed via Puppeteer/Chromium headless: `alert(document.domain)` fires.**\n\nValidated on Wallabag (which uses Graby) via Docker: entry created via API with iframe-only content body triggers Readability failure → falls through to `cleanupXss()` path.\n\n## Impact\n\n- Stored XSS in any application rendering Graby-sanitized content via `|raw`\n- In Wallabag: affects both authenticated views and public share pages (unauthenticated)\n- No CSP headers in default Wallabag config — no secondary mitigation\n\n## Suggested Fix\n\nEither remove `+iframe` from the elements config to keep iframes blocked:\n```php\n'elements' => '*-iframe-meta',\n```\n\nOr explicitly deny the `srcdoc` attribute:\n```php\n'deny_attribute' => 'style srcdoc',\n```\n\n## Credit\n\nDiscovered by @tikket1, 2026-03-25. Redirected from wallabag/wallabag advisory by @j0k3r.","severity":"low","score":2.1,"published":"2026-03-31T23:12:36.000Z","modified":"2026-03-31T23:32:26.906Z","withdrawn":false,"knownExploited":false,"affected":[{"ecosystem":"Packagist","name":"j0k3r/graby","ranges":[{"events":[{"introduced":"0"},{"fixed":"2.5.1"}],"type":"ECOSYSTEM"}],"versions":["1.0.0","1.0.0-alpha.0","1.0.0-alpha.1","1.0.0-alpha.2","1.0.1","1.0.2","1.0.3","1.0.4","1.0.5","1.0.6","1.0.7","1.0.8","1.1.0","1.10.0","1.10.1","1.11.0","1.11.1","1.12.0","1.12.1","1.13.0","1.13.1","1.13.2","1.13.3","1.13.4","1.13.5","1.13.6","1.14.0","1.15.0","1.15.1","1.15.2","1.15.3","1.15.4","1.15.5","1.16.0","1.17.0","1.18.0","1.18.1","1.19.0","1.19.1","1.2.0","1.20.0","1.20.1","1.3.0","1.4.0","1.4.1","1.4.2","1.4.3","1.4.4","1.4.5","1.5.0","1.5.1","1.5.2","1.5.3","1.5.4","1.6.0","1.6.1","1.6.2","1.7.0","1.7.1","1.8.0","1.8.1","1.8.2","1.9.0","1.9.1","1.9.2","1.9.3","2.0.0","2.0.0-alpha.0","2.0.1","2.0.2","2.1.0","2.1.1","2.2.0","2.2.1","2.2.4","2.2.5","2.2.6","2.2.7","2.3.0","2.3.1","2.3.2","2.3.3","2.3.4","2.3.5","2.4.0","2.4.1","2.4.2","2.4.3","2.4.4","2.4.5","2.4.6","v2.2.2","v2.2.3","v2.5.0"],"fixed":["2.5.1"]}],"references":["https://github.com/j0k3r/graby/security/advisories/GHSA-3h6j-9x8m-rg3g","https://github.com/j0k3r/graby/commit/0295d828822f7a59c5751a8199973a4f965a99b0","https://github.com/j0k3r/graby","https://github.com/j0k3r/graby/releases/tag/2.5.1"],"sources":[{"name":"OSV","id":"GHSA-3h6j-9x8m-rg3g","modified":"2026-03-31T23:32:26.906898Z","url":"https://osv.dev/vulnerability/GHSA-3h6j-9x8m-rg3g"}],"assessments":[{"source":"OSV","vector":"CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:P/VC:N/VI:N/VA:N/SC:L/SI:L/SA:N/E:P","score":2.1}]}