-----BEGIN PGP SIGNED MESSAGE----- Hash: SHA256 NEFARIOUSPLAN-CANONICAL-V1 {"body_md":"## The advisory says the fix is in 2.4.4\n\n[CVE-2026-41472](https://nvd.nist.gov/vuln/detail/CVE-2026-41472) was published on April 24, 2026. The bug, paraphrased from the published description:\n\n> CyberPanel versions prior to 2.4.4 contain a stored cross-site scripting vulnerability in the AI Scanner dashboard. The endpoint POST /api/ai-scanner/callback is unauthenticated. An unauthenticated request overwrites the findings_json field of a ScanHistory record with arbitrary JavaScript. When the administrator opens the AI Scanner dashboard, the payload executes in their authenticated session, issues a same-origin request to /websites/addNewCron, plants a cron job, and reaches remote code execution on the server.\n\nThe cve.org `affected` block points downstream consumers at a specific commit:\n\n```\nversionType: semver\nversion: 0\nlessThan: 2.4.4\nstatus: affected\n\nversion: 0a099b1b193946555fbdd387a28486b1521f9961\nversionType: git\nstatus: unaffected\n```\n\nVulnerability tooling that ingests this record concludes that any install at or after `0a099b1b` is patched. The CVSS metadata, the description, and the cited unaffected commit are the structured statement that 2.4.4 ships the fix. CyberPanel, the cve.org record, the NVD, and [VulnCheck's advisory](https://www.vulncheck.com/advisories/cyberpanel-stored-xss-via-ai-scanner-dashboard) all repeat the same hash.\n\n## What commit 0a099b1b actually contains\n\n```diff\ncommit 0a099b1b193946555fbdd387a28486b1521f9961\nMerge: 94bf2bde a2f9cf99\nAuthor: usmannasir \nDate: Thu Dec 18 13:10:48 2025 +0500\n\n Merge branch 'v2.4.4' of github.com:usmannasir/cyberpanel into v2.4.4\n\n--- a/plogical/acl.py\n+++ b/plogical/acl.py\n@@ -761,6 +761,8 @@ class ACLManager:\n else:\n if childDomain.master.admin.owner == admin.pk:\n return 1\n+ else:\n+ return 0\n\n except:\n domainName = Websites.objects.get(domain=domain)\n\n--- a/websiteFunctions/website.py\n+++ b/websiteFunctions/website.py\n@@ -2519,11 +2519,12 @@ Require valid-user\n for web in websites:\n- for child in web.childdomains_set.filter(alais=0):\n- if child.domain == f'mail.{web.domain}':\n- pass\n- else:\n- childDomains.append(child)\n+ for child in web.childdomains_set.all():\n+ if child.alais == 0:\n+ if child.domain == f'mail.{web.domain}':\n+ pass\n+ else:\n+ childDomains.append(child)\n```\n\nTwo changes. The first adds a missing `return 0` to an ACL ownership check that previously fell through silently. The second restructures a child-domain iteration so the filter happens in Python rather than at the ORM. Both fix an ACL bug for non-admin users managing child domains, tracked under a separate report. Neither is the AI Scanner.\n\nThe merge brings in one further commit on the parent line, labeled `security fixes`:\n\n```\ncommit eca0c3cbeb35af8eaae9fafb094e8ef3cd923643\nAuthor: usmannasir \nDate: Thu Dec 18 12:18:32 2025 +0500\n\n security fixes\n\n backup/backupManager.py | 95 +++++++++++++++++++++++++++++++++++++++++-------\n filemanager/views.py | 49 +++++++++++++++++++++++++\n plogical/remoteBackup.py | 34 +++++++++++++++--\n```\n\nBackup manager. File manager. Remote backup. None of them is the AI Scanner.\n\nTraversing both parents of the cited merge, no commit in 2.4.4's lineage modifies `aiScanner/api.py`, `aiScanner/views.py`, `aiScanner/status_api.py`, `aiScanner/templates/aiScanner/scanner.html`, or `CyberCP/secMiddleware.py`. The cited fix is a fix for a different bug.\n\n## What the actual bug is\n\n`/api/ai-scanner/callback` is wired in `api/urls.py`:\n\n```python\nre_path(r'^ai-scanner/callback$', views.aiScannerCallback, name='aiScannerCallbackAPI'),\n```\n\n`aiScannerCallback` delegates to `aiScanner/api.py:scan_callback`:\n\n```python\n@csrf_exempt\n@require_http_methods(['POST'])\ndef scan_callback(request):\n \"\"\"\n Receive scan completion callbacks from AI Scanner platform\n\n POST /api/ai-scanner/callback\n \"\"\"\n try:\n data = json.loads(request.body)\n\n scan_id = data.get('scan_id')\n status = data.get('status')\n summary = data.get('summary', {})\n findings = data.get('findings', [])\n ...\n\n scan_record = ScanHistory.objects.get(scan_id=scan_id)\n scan_record.status = status\n ...\n scan_record.set_findings(findings)\n ...\n scan_record.save()\n```\n\nThere is no session check. There is no API key check. There is no HMAC over the body. There is no source-IP gate. The handler reads attacker-controlled JSON and writes it into a `ScanHistory` row keyed by an attacker-supplied `scan_id`.\n\n`set_findings` serialises the input straight to the database column the dashboard reads:\n\n```python\ndef set_findings(self, findings_list):\n \"\"\"Set findings from list/dict\"\"\"\n self.findings_json = json.dumps(findings_list)\n```\n\nWhen an administrator next opens the AI Scanner dashboard's scan-details view (which is gated by session and ACL), the front-end fetches the stored findings and renders them via JavaScript template literal:\n\n```javascript\nscan.findings.forEach(finding => {\n const severityClass = finding.severity === 'high' ? 'danger'\n : finding.severity === 'medium' ? 'warning'\n : 'info';\n\n findingsHtml += `\n
\n
\n ${finding.severity}\n ${finding.title}\n
\n

${finding.description}

\n ${finding.file ? `File: ${finding.file}` : ''}\n
\n `;\n});\n...\ndocument.getElementById('completedSection').innerHTML = `... ${findingsHtml} ...`;\n```\n\n`finding.title`, `finding.description`, and `finding.file` interpolate directly into HTML, and the result is assigned to `.innerHTML`. The CVE description calls this stored XSS. The exit from the dashboard into the host is the same-origin POST to `/websites/addNewCron` under the admin's session: cron jobs run as the website user, and the payload writes its own.\n\n## Why the callback is unauthenticated by structure\n\n`CyberCP/secMiddleware.py` opens with an unconditional bypass:\n\n```python\nif pathActual == \"/backup/localInitiate\" or pathActual == '/' or pathActual == '/verifyLogin' or pathActual == '/logout' or pathActual.startswith('/api')\\\n or webhook_pattern.match(pathActual) or pathActual.startswith('/cloudAPI'):\n pass\nelse:\n try:\n val = request.session['userID']\n except:\n ...\n```\n\nAny URL whose path starts with `/api` skips the session check at the middleware layer. The line has been there since the initial public commit on August 1, 2025 (`6dd7114f`). Every handler under `/api/*` is responsible for its own authentication. The AI Scanner's callback handlers do not perform any.\n\nThis is the structural reason the bug has the shape it has. The author wrote `@csrf_exempt` because the external scanner platform calling in will not carry a Django CSRF token, and assumed the same exemption covered the same threat model the CSRF protection covers. It does not. CSRF protects authenticated state-changing actions from cross-origin abuse; authentication is what protects state-changing actions from arbitrary unauthenticated callers. Removing the first did not establish the second.\n\nThe sibling endpoint `/api/ai-scanner/status-webhook` lands in `aiScanner/status_api.py:receive_status_update` with the same shape:\n\n```python\n@csrf_exempt\n@require_http_methods(['POST'])\ndef receive_status_update(request):\n ...\n status_update, created = ScanStatusUpdate.objects.update_or_create(\n scan_id=scan_id,\n defaults={\n 'phase': data.get('phase', ''),\n ...\n 'current_file': data.get('current_file', ''),\n 'activity_description': data.get('activity_description', ''),\n ...\n }\n )\n```\n\n`current_file` and `activity_description` are attacker-controlled strings that render into the live-progress dashboard. Same primitive, different table. The `update_or_create` accepts any `scan_id`; the attacker no longer needs to know an existing one to seed an XSS payload that the admin's progress view will render.\n\n## The current stable still ships the bug\n\nCyberPanel's stable branch as of March 6, 2026 is at version 2.4.5. `version.txt`:\n\n```json\n{\"version\":\"2.4\",\"build\":5}\n```\n\nThe git log for each vulnerable file on stable:\n\n```\n$ git log --oneline -- aiScanner/api.py | head -5\n37cc3e42 Increase rate limits for bulk fix operations from platform\n2cd361a8 Fix backup file operation failures for API key authentication\naf35aadb Fix CyberPanel API key validation for platform callbacks\ncfeac425 Add X-API-Key header support for AI Scanner file operations\na60c4827 Add debug logging for API key validation\n```\n\n```\n$ git log --oneline -- aiScanner/status_api.py\n6dd7114f Initial commit for v2.4.3\n```\n\n```\n$ git log --oneline -- aiScanner/templates/aiScanner/scanner.html\n1b045eb2 enable dark mode\n6dd7114f Initial commit for v2.4.3\n```\n\n```\n$ git log --oneline -- CyberCP/secMiddleware.py | head -3\ne65109fe Fix security middleware to allow file content in API endpoints\n6dd7114f Initial commit for v2.4.3\n```\n\n`aiScanner/status_api.py` has not been modified since it was first committed. `aiScanner/templates/aiScanner/scanner.html` has been touched once, for dark mode. `CyberCP/secMiddleware.py`'s most recent change, on October 26, 2025, expanded the `/api/*` bypass list to additionally skip command-injection validation on fields named `content` and `fileContent`. The `/api/*` line at the top of the middleware is untouched.\n\nThe X-API-Key support in `aiScanner/api.py` (commit `cfeac425`, October 27) added authentication to file-operation endpoints like `/api/scanner/replace-file`. It did not add authentication to `/api/ai-scanner/callback`. A follow-up commit the same day (`af35aadb`, \"Fix CyberPanel API key validation for platform callbacks\") then relaxed the new check, with the commit message stating the intent in the open:\n\n> Get the scan, don't require it to belong to the same admin\n> (platform may be using any valid CyberPanel API key for file operations)\n\nBetween December 16, 2025 (the date the bug was reported to the vendor) and May 16, 2026, the number of commits on the public stable branch that touch `aiScanner/api.py`, `aiScanner/status_api.py`, `aiScanner/views.py`, `aiScanner/templates/aiScanner/scanner.html`, or `CyberCP/secMiddleware.py` to add authentication to the callback path or to escape attacker input in the dashboard is zero.\n\n## Two researcher attributions, one timeline that does not add up\n\nVulnCheck's advisory credits \"Djibril Mounkoro.\" A separate technical writeup at [itsrez.re/post/cyberpanel-rce](https://itsrez.re/post/cyberpanel-rce) describes the same chain and credits \"itsrez (in collaboration with Mcsam).\" The itsrez writeup includes a coordinated disclosure timeline: reported December 16, 2025; acknowledged December 17; fixes committed December 19; user notification January 1, 2026; public disclosure January 18.\n\nThe cited commit `0a099b1b` is dated December 18, between the reported date and the writeup's claimed fix-committed date. The git log on the public stable branch for that week shows three commits: `eca0c3c` (security fixes, touching backup and file manager), `94bf2bd` (a merge), and `0a099b1b` (the cited merge). No commit on December 19 touches the AI Scanner. The itsrez writeup describes the patch as \"added explicit HTML escaping using Django's `|escape` filter on all user-controlled scan result fields\" and \"implemented API key validation against database records.\" That code is not in the public tree.\n\nTwo researcher attributions on the same bug. One published writeup that describes a patch that does not exist on the public branch. A CVE record whose cited unaffected commit modifies different files. A vendor stable branch that still ships the bug, five months after the writeup said it had been fixed.\n\n## The pattern, named twice\n\nThis is denial-by-pedantry in the CVE record itself. The advisory's cited fix commit exists; the version named as unaffected is released; the customer's natural reading of \"upgrade to 2.4.4 and the bug is closed\" is not what the cited evidence supports when the cited evidence is read. The structured `unaffected` field is the part of the CVE record that downstream tools rely on most, and it is the part that resists being checked because checking it requires reading a diff against a separate bug. The advisory is true within its narrow scope. The bug ships intact. This is the same shape as the [Oracle Cloud breach Oracle technically didn't deny](/posts/oracle-cloud-the-breach-they-technically-didnt-deny) and [SmartScreen's narrow scope of \"downloaded from the internet\"](/posts/cve-2025-33053-the-other-half-got-a-doc-edit), encoded in the structured metadata that vulnerability scanners read instead of in the press statement that humans read.\n\nIt is also the-detector-is-the-target. CyberPanel's AI Scanner is the security tool. It accepts website scan submissions, dispatches them to the remote scanner platform, and renders findings to administrators so they know what was found on their sites. The trust boundary between the platform and CyberPanel, the callback endpoint where the findings arrive, was built without authentication. The administrator's view of \"what malware did\" is the channel through which the unauthenticated caller does what they want.\n\nThe CVE record points downstream tooling at a commit hash. The hash matches in 2.4.4. The match is real. The fix is not.\n\nPoC: [@secdb on infosec.exchange](https://infosec.exchange/@secdb/116473729986688714). Writeup: [itsrez.re/post/cyberpanel-rce](https://itsrez.re/post/cyberpanel-rce).","closing_line":"The hash matches in 2.4.4. The match is real. The fix is not.","hook_md":"The CVE record for CVE-2026-41472 names commit `0a099b1b193946555fbdd387a28486b1521f9961` as the version that is unaffected. The commit is a merge dated December 18, 2025. It modifies two files: `plogical/acl.py` and `websiteFunctions/website.py`. Neither file is involved in the bug. The bug lives in `aiScanner/api.py`, `aiScanner/templates/aiScanner/scanner.html`, and `CyberCP/secMiddleware.py`. The current stable branch is 2.4.5. None of those three files has been modified since the bug was disclosed. The commit landed in 2.4.4. The fix did not.","post_id":311,"slug":"cyberpanel-cve-2026-41472-cited-commit-does-not-touch-the-bug","title":"CVE-2026-41472: The Commit CyberPanel Cited as the Fix Does Not Touch the AI Scanner","type":"initial","unreadable_sentence":"The CVE record points downstream tooling at a commit hash. The hash matches in 2.4.4. The match is real. The fix is not."} -----BEGIN PGP SIGNATURE----- iHUEARYIAB0WIQRf0htP5+SjynlxywneZjl4jgkQJgUCavdSLgAKCRDeZjl4jgkQ Jp5uAPwO6vSkOM0ilP6zu4bnzPOKux65MaX6sbIkz970KkehNQEAtI4PHNNzx6P3 0RetQSgetObkkqu+TIPGpni/jjk9kAg= =/yvg -----END PGP SIGNATURE-----