AdminNeo SQLite Database-List Drop Arbitrary File Deletion
CVE-2026-96633 · GHSA-6vfx-w7q3-42gq · published 30 Oct 2026
How serious is it
- Severity: high, CVSS score 8.1 out of 10.
- No exploit prediction (EPSS) score yet.
- Not on the CISA list of exploited vulnerabilities.
- No public exploit in Exploit-DB.
Is your app affected by CVE-2026-96633?
Paste this prompt into Claude Code, Cursor or any AI coding tool: it runs a free Shippable scan of your project and fixes what it finds. Built with Lovable, Bolt or v0? Scan your live app. Or upload your package file.
Who is affected
adminneo-org/adminneo(Packagist) < 5.5.1
How to check
- Run
composer show adminneo-org/adminneoin your project to see the installed version, including copies pulled in by other packages. - Or paste your lockfile into the checker: it lists every package with a known vulnerability, this one included.
How to fix
Copies a prompt for Claude Code, Cursor or Codex: it checks your lockfiles for this and upgrades it.
- Upgrade
adminneo-org/adminneoto 5.5.1 or later, commit the lockfile and redeploy. - If a vulnerable version was live, check your logs and rotate the secrets that server could read.
The advisory
## 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.txt
```
The same directory also contained a SQLite database:
```
/var/www/html/poc.sqlite
```
### 3. 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.
Check your whole app
Dependencies are only half of it. Free and read-only: before you push, run npx shippable-dev scan localhost:3000 in your project folder, no account needed. For your live app, you prove it's yours with a one-line tag first.
References
- github.com/adminneo-org/adminneo/security/advisories/GHSA-6vfx-w7q3-42gq
- github.com/adminneo-org/adminneo/commit/26fcef2f32922629751cbea6c0bb593fbf1e957d
- github.com/adminneo-org/adminneo/releases/tag/v5.5.1
- github.com/advisories/GHSA-6vfx-w7q3-42gq
- github.com/adminneo-org/adminneo
Merged from GitHub Advisory Database, OSV. Last updated 31 Oct 2026. All vulnerabilities · RSS · JSON