PT-2026-134414
CVE-2026-96633
·
Published
2026-10-30
·
Updated
2026-10-30
Description by
CVSS 3.1
8.1
HighMetricsAV:N/AC:L/PR:L/UI:N/S:U/C:N/I:H/A:H
Summary
AdminNeo allows an authenticated SQLite-mode user to delete arbitrary files writable by the PHP process through the database-list drop action. The SQLite driver validates file extensions when creating or renaming databases, but the database-list drop path passes submitted
db[] values directly to unlink().Originally reported by @slpoiuewalspoid-dotcom in Adminer: https://github.com/vrana/adminer/security/advisories/GHSA-6pg3-chwq-wgqc
Impact
An authenticated AdminNeo SQLite user with a valid CSRF token can submit an arbitrary relative file path in the database-list
db[] field. If the PHP process can delete that path, AdminNeo unlinks it even when the file is not a SQLite database.Root Cause
The database-list page accepts selected database names from the POST body and calls
drop databases():php
if ($ POST["db"]) {
queries redirect(substr(ME, 0, -1), lang('Databases have been dropped.'), drop databases($ POST["db"]));
}In SQLite mode,
drop databases() attaches an in-memory database and unlinks each submitted path directly:php
function drop databases($databases): bool
{
Connection::get()->close(); // to unlock file, doesn't work in PDO on Windows
foreach ($databases as $db) {
if (!@unlink($db)) {
Connection::get()->setError(lang('File exists.'));
return false;
}
}
return true;
}The same SQLite driver protects create and rename operations with
check sqlite name(), which requires a .db, .sdb, or .sqlite extension:php
function check sqlite name($name) {
// avoid creating PHP files on unsecured servers
$extensions = "db|sdb|sqlite";
if (!preg match("~^[^0]*.($extensions)$~", $name)) {
Connection::get()->setError(lang('Please use one of the extensions %s.', str replace("|", ", ", $extensions)));
return false;
}
return true;
}The drop path does not apply an equivalent extension check or bind the submitted path to a known listed SQLite database.
POC
1. Start a complete AdminNeo application
Build and start AdminNeo from the audited source using the official standalone Docker image shape. The validation image copies the audited source tree into
/var/www/html and starts Nginx HTTP server.Log in through the SQLite entry point using a SQLite database path
poc.sqlite.2. Prepare a writable target file
Place a non-database file under a location writable by the AdminNeo PHP process. The validation used:
/var/www/html/poc-delete-target.txtThe same directory also contained a SQLite database:
/var/www/html/poc.sqlite3. Submit the database-list drop request
Use the authenticated session and live CSRF token, but submit the non-database path as the selected database:
http
POST /?sqlite=&username= HTTP/1.1
Host: <adminer-host>
Cookie: <authenticated-session>
Content-Type: application/x-www-form-urlencoded
db[]=/var/www/html/poc-delete-target.txt&all=&drop=Drop&token=<live-csrf-token>The request reaches
drop databases($ POST["db"]), and SQLite drop databases() calls unlink() on the submitted non-database path.After the request, the
poc-delete-target.txt was deleted.Suggested Fix
Apply the same target validation used by SQLite create/rename before deleting database files. At minimum:
- Reject paths whose basename does not end in
.db,.sdb, or.sqlite. - Bind
db[]values to databases returned by AdminNeo's database listing instead of trusting submitted paths. - Resolve paths before deletion and reject traversal outside the expected SQLite database directory.
- Consider requiring the current selected database path for delete operations instead of accepting arbitrary submitted file paths.
Found an issue in the description? Have something to add? Feel free to write us 👾