- Advisory: GHSA-7pj6-f27j-wmcw · CVE-2026-75609
- Affected:
egroupware/egroupware< 26.7.20260710 and < 23.1.20260710 · Fixed: 26.7.20260710 / 23.1.20260710 - Severity: Medium · CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:L/I:L/A:N = 6.1 · CWE-601
- Status: publicly disclosed and fixed. Reported by Pig-Tail through coordinated disclosure.
login.php reads the attacker-supplied phpgw_forward parameter and, after a successful login,
redirects the browser to it. Api\Session::link() is supposed to keep redirects local by prefixing
the configured webserver_url — but its guard only applies that prefix when the target does not
already contain "$webserver_url/" as a substring:
// api/src/Session.php::link()
if (($url[0] != '/' || $webserver_url != '/') &&
(!$webserver_url || strpos($url, $webserver_url.'/') === false)) { /* prefix webserver_url */ }With the default webserver_url = /egroupware, an absolute attacker URL that simply embeds that
literal substring — https://evil.com/egroupware/ — makes strpos(...) !== false, the guard is
skipped, and the URL is used verbatim as the Location. The app-prefix at the top of link()
($url = $app.'/'.$url) is also skipped, because currentapp == 'login' on login.php, so the
absolute URL is not mangled either.
Result: Location: https://evil.com/egroupware/?cd=yes.
Alternate vector: on deployments with webserver_url of / or empty (reverse-proxy / docker
roots), phpgw_forward=//evil.com/ is protocol-relative and passes the same guard.
- Attacker sends the victim
…/login.php?phpgw_forward=https://evil.com/egroupware/. - Victim authenticates — the login form action carries
phpgw_forward(api/src/Framework/Login.php), so it survives the POST, andparseForwardalso reads it from POST. parseForward→forward = https://evil.com/egroupware/,extra_vars = cd=yes.redirect_link→link(): app prefix skipped (currentapp=login);webserver_urlprefix skipped because the URL contains/egroupware/;enforce_ssl, where set, only rewriteshttp:→https:and preserves the host — there is no host allowlist anywhere on the path.api/src/Framework.php:212→Header("Location: https://evil.com/egroupware/?cd=yes").
Credential phishing and OAuth/redirect-chain abuse: a link to the legitimate EGroupware host
lands the user on an attacker page immediately after login, defeating the trust signal of the real
login domain. On SSO installs — where Api\Auth::login() returns a session with no credential entry
— the redirect can fire with no interactive login at all.
This is redirect-only, not XSS: the value reaches a Location: header, and javascript: / data:
schemes in Location are ignored by modern browsers.
poc/open_redirect_poc.php reproduces Session::link()'s guard verbatim and prints the Location
header Framework::redirect() would emit, for both attack vectors plus two negative controls.
Fully offline — no network traffic and no EGroupware installation:
php poc/open_redirect_poc.php # PHP 8.0+webserver_url |
phpgw_forward |
Location: |
Result |
|---|---|---|---|
/egroupware |
https://evil.com/egroupware/ |
https://evil.com/egroupware/ |
off-site |
/egroupware |
https://evil.com/ |
/egroupwarehttps://evil.com/ |
stays local |
| `` (root install) | //evil.com/ |
//evil.com/ |
off-site |
/egroupware |
/index.php?menuaction=home |
/egroupware/index.php?menuaction=home |
stays local |
The negative controls matter: the guard is not simply absent. It works — right up until the attacker's URL contains the configured path as a substring, which the attacker fully controls.
In Session::link() / the login forward handling, reject any phpgw_forward that is absolute or
protocol-relative (//), or validate its host against an allowlist containing only the configured
webserver_url host. Do not use strpos(webserver_url.'/') as a locality test — the attacker owns
the rest of the URL. Canonicalize to a path-only forward.