---
id: "CVE-2026-96633"
kind: "vulnerability"
aliases: ["CVE-2026-96633", "GHSA-6vfx-w7q3-42gq"]
title: "AdminNeo SQLite Database-List Drop Arbitrary File Deletion"
severity: "high"
cvss: 8.1
epss: null
exploited: false
status: "open"
category: "package"
products: [{"name": "adminneo-org/adminneo", "fixed": ["5.5.1"], "affected": "< 5.5.1", "ecosystem": "composer"}]
ecosystems: ["composer"]
tags: ["adminneo", "cwe-73", "package", "php", "self-hosted", "sqlite"]
not_affected_if: ["auth-required", "default-config"]
published_at: "2026-10-30T22:52:58+00:00"
first_seen_at: "2026-10-30T22:56:53+00:00"
updated_at: "2026-10-30T22:58:39+00:00"
sources: [{"source": "ghsa", "url": "https://github.com/advisories/GHSA-6vfx-w7q3-42gq", "first_seen_at": "2026-10-30T22:56:53+00:00"}]
---

# AdminNeo SQLite Database-List Drop Arbitrary File Deletion

AdminNeo's SQLite mode trusts file paths submitted to its database-list drop action and passes them straight to unlink(). An authenticated SQLite-mode user can delete any file the PHP process can write, including non-database files.

_Summary, check and fix written by a model from the sources; labelled as such._

## Affected

- composer:adminneo-org/adminneo affected < 5.5.1 fixed 5.5.1

## How to check

- Check the AdminNeo version with `composer show adminneo-org/adminneo` and see if it is below `5.5.1`.
- Check whether the app is served with the SQLite entry point enabled (`?sqlite=`).

## Fix

- `adminneo-org/adminneo` (composer): move to `5.5.1` or later.

## Timeline

- 2026-10-30T22:56:53+00:00 ghsa https://github.com/advisories/GHSA-6vfx-w7q3-42gq

## Documents

- [advisory] Maintainer's advisory: api.github.com https://api.github.com/repos/adminneo-org/adminneo/security-advisories/GHSA-6vfx-w7q3-42gq
- [advisory] Maintainer's advisory: github.com https://github.com/adminneo-org/adminneo/security/advisories/GHSA-6vfx-w7q3-42gq
- [fix] Patch: github.com https://github.com/adminneo-org/adminneo/commit/26fcef2f32922629751cbea6c0bb593fbf1e957d
- [fix] Release: github.com https://github.com/adminneo-org/adminneo/releases/tag/v5.5.1

## From the sources

## 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.

---

_Free to read at a human pace. An API with keys and an MCP server are in preparation; early access: hello@infrastack.ai_
