GitHub Security Advisory: GHSA-x3hr-cp7x-44r2
Package: ci4-cms-erp/ci4ms (COMPOSER)
Severity: MODERATE (CVSS 5.5)
CWE: CWE-79: Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting')
Vulnerable versions: <= 0.31.3.0
Patched in: 0.31.4.0

Summary: CI4MS has stored XSS via srcdoc attribute bypass in Google Maps iframe setting

Details:
## Summary

The 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.

## Details

**Input sanitization** (`modules/Settings/Controllers/Settings.php:49-53`):

```php
$mapValue = trim(strip_tags($this->request->getPost('cMap'), '<iframe>'));
$mapValue = preg_replace('/\bon\w+\s*=\s*"[^"]*"/i', '', $mapValue);
$mapValue = preg_replace('/\bon\w+\s*=\s*\'[^\']*\'/i', '', $mapValue);
$mapValue = preg_replace('/\bon\w+\s*=\s*[^\s>]+/i', '', $mapValue);
setting()->set('Gmap.map_iframe', $mapValue);
```

The 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.

**Output rendering** (`app/Views/templates/default/gmapiframe.php:3`):

```php
<?php echo strip_tags($settings->map_iframe,'<iframe>') ?>
```

The output applies `strip_tags` with the same `<iframe>` allowlist but performs no attribute filtering or HTML encoding. The stored payload is rendered verbatim.

**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>`.

**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.

## PoC

**Prerequisites**: Authenticated admin session with `update` role on the Settings module.

**Step 1: Inject the payload**

```bash
curl -X POST 'https://target/backend/settings/compInfos' \
  -H 'Cookie: ci_session=ADMIN_SESSION_ID' \
  -d 'cName=TestCo&cAddress=123+Main+St&cPhone=[PII_REDACTED]&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'
```

The `cMap` value decodes to:
```html
<iframe srcdoc="&lt;script&gt;alert(document.domain)&lt;/script&gt;"></iframe>
```

**Step 2: Visit any public page that includes the Google Maps widget**

Navigate 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.

**Expected result**: JavaScript `alert(document.domain)` fires showing the targe