Skip to content

SQLite3 driver runs write queries via SQLite3::exec(), allowing stacked statements #10652

Closed

Description

@secretj

PHP Version

7.4.33

CodeIgniter4 Version

Reproduced on 4.0.4. The same execute() is present on current develop: https://github.com/codeigniter4/CodeIgniter4/blob/develop/system/Database/SQLite3/Connection.php

CodeIgniter4 Installation Method

Composer (manual)

Which operating systems have you tested for this bug?

macOS

Which server did you use?

cli

Environment

Development

Database

SQLite3

What happened?

CodeIgniter\Database\SQLite3\Connection::execute() runs write-type queries through SQLite3::exec(), which executes multiple ;-separated statements, while read queries go through single-statement SQLite3::query():

protected function execute(string $sql)
{
    return $this->isWriteType($sql)
        ? $this->connID->exec($sql)   // executes stacked statements
        : $this->connID->query($sql);
}

Query bindings are the correct defense, and this is not a vulnerability in the framework itself. But when application code concatenates untrusted input into a write query (a common mistake), the SQLite3 driver turns a single mistaken statement into stacked-query execution: an attacker can append ; DROP TABLE ..., or ; ATTACH DATABASE '/path/x.db' AS v; ... to create a file at an arbitrary writable path and read/write other SQLite databases the process can reach. Most other drivers do not execute stacked statements from a single mistaken write, so the blast radius here is larger and specific to this driver.

Steps to Reproduce

$db->query("CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)");
$db->query("INSERT INTO t VALUES (1, 'a')");

$evil = "x'; CREATE TABLE marker(a); --";
// application code that forgot to bind the value:
$db->query("UPDATE t SET name = '{$evil}' WHERE id = 1");

// table `marker` now exists — the appended statement ran.

Expected Output

A single write query executes a single statement. Statements appended after ; inside the string should not be executed, matching the behavior of the read path (SQLite3::query()) and of other drivers.

Anything else?

Proposal (for discussion): route write-query execution through a single-statement path — for example SQLite3::query() plus SQLite3::changes() for the affected-row count, or a prepared statement — instead of exec().

Backward compatibility: some applications may intentionally run multiple ;-separated statements in a single write (schema / migration / seed scripts). If a single-statement write path is undesirable, documenting the stacked-statement behavior of the SQLite3 driver would still help users reason about the risk.

Activity

  1. carson-codeigniter4 commented on Oct 23, 2026

    @carson-codeigniter4
  2. michalsn commented on Oct 23, 2026

    @michalsn
    Member

    User-supplied values should be passed through query bindings rather than concatenated into SQL. Multiple-statement execution is driver-dependent and can be used intentionally to run trusted SQL batches. Restricting execution to a single statement would not make concatenated user input safe, since SQL injection can also alter the original statement.

    Let's see what others think, but I'm leaning toward keeping the current behavior.

  3. secretj commented on Oct 23, 2026

    @secretj
    Author

    for the thoughtful response.
    You're right on all three points - single-statement execution wouldn't make concatenated input safe either way, and multi-statement writes are an intentional feature.
    Bindings are the real fix. I'm convinced; happy for this to be closed. Thanks again.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions