SQLite requires that an authorizer callback not modify the connection
that invoked it, and counts sqlite3_prepare_v2() and sqlite3_step() as
modifications. node:sqlite let the callback call prepare(), exec(), the
statement execution methods, and other connection-mutating APIs on the
same DatabaseSync. Track authorizer depth on DatabaseSync with an RAII
guard around the callback and throw ERR_INVALID_STATE from the affected
entry points while it is on the stack. Covering every authorizer
invocation, including the re-prepare that SQLite can run during
sqlite3_step(), exposed a second and distinct hazard: reentering a
statement that is currently being stepped is a use-after-free rather
than a contract violation, since finalizing it frees the virtual machine
under sqlite3_step() and re-running it resets that machine
mid-execution. Any callback SQLite invokes during execution can reach
it, so a user-defined function is enough. Track the statements currently
being stepped and reject reentry into only those, which leaves a
user-defined function free to prepare, run, and finalize its own helper
statements.
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
Fixes: https://github.com/nodejs/node/issues/63207
Assisted-by: claude:opus-5
PR-URL: https://github.com/nodejs/node/pull/65156
Reviewed-By: Xuguang Mei <meixuguang@gmail.com>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
Apply the same check to DatabaseSync::Prepare() so that statement-less
SQL is rejected at preparation instead of on first use. This matches
SQLite's own oo1 JavaScript API, which throws when the SQL contains no
statements rather than exposing the C API's null statement pointer.
Previously db.prepare('-- comment') returned a StatementSync whose
statement_ was null. Every method on it threw "statement has been
finalized", which was misleading because nothing had been finalized, and
the object was still inserted into statements_. Since IsFinalized() is
true for a null statement, its destructor skipped UntrackStatement() and
left a dangling pointer in the set that a later close() would finalize.
Refs: https://github.com/nodejs/node/pull/65157#discussion_r3742903347
Refs: https://sqlite.org/wasm/doc/trunk/api-oo1.md
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65157
Fixes: https://github.com/nodejs/node/issues/65149
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
sqlite3_prepare_v2() returns SQLITE_OK without producing a statement
when its input holds no SQL, such as a comment. PrepareStatement() only
checked the return code, so it cached a StatementSync wrapping a null
sqlite3_stmt. Executing it reached sqlite3_clear_bindings(), which only
guards against a null statement under SQLITE_ENABLE_API_ARMOR, and
segfaulted.
Reject such input instead of caching it. The StatementSync methods
already avoid the crash because their IsFinalized() guard treats a null
statement as finalized.
Fixes: https://github.com/nodejs/node/issues/65149
Signed-off-by: Trevor Burnham <trevorburnham@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/65157
Fixes: https://github.com/nodejs/node/issues/65149
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
sqlite3_column_text() can return nullptr on failure which
was not handled.
sqlite3_column_blob() can return nullptr for zero-length BLOBs,
which is then passed to memcpy() which is UB.
Avoid this by checking for a nullptr.
Signed-off-by: ndossche <nora.dossche@ugent.be>
PR-URL: https://github.com/nodejs/node/pull/63288
Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
This extends explicit resource management support to prepared
statements, allowing a StatementSync to be deterministically
finalized via a `using` declaration, mirroring the existing
DatabaseSync and Session dispose methods.
Signed-off-by: Guilherme Araújo <arauujogui@gmail.com>
PR-URL: https://github.com/nodejs/node/pull/64232
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
DatabaseSync::DeleteSessions() deleted native sqlite3_session handles
without clearing the pointers held by their Session wrappers. Reopening
the database allowed stale session handles to be used, crashing the
process.
Track Session wrappers and delete sessions through Session::Delete() so
the wrappers are marked closed.
Signed-off-by: Kamat, Trivikram <16024985+trivikr@users.noreply.github.com>
Assisted-by: codex:gpt-5.6-sol
PR-URL: https://github.com/nodejs/node/pull/64783
Fixes: https://github.com/nodejs/node/issues/64782
Reviewed-By: Aviv Keller <me@aviv.sh>
SQLTagStore Run/Iterate/Get/All reset the shared cached statement via
raw sqlite3_reset(), bypassing ResetStatement() and its reset_generation_
bump, so live iterators were never invalidated. Also mark the iterator
done on SQLITE_DONE so it cannot restart the statement.
PR-URL: https://github.com/nodejs-private/node-private/pull/896
Refs: https://hackerone.com/reports/3564941
Reviewed-By: Robert Nagy <ronagy@icloud.com>
CVE-ID: CVE-2026-58041
When sqlite3_exec() or sqlite3changeset_apply() call JavaScript
callbacks (user-defined functions, conflict handlers, or filter
callbacks), the DatabaseSync object could be garbage-collected
if the JavaScript code drops all references to it. Both methods
only held a raw DatabaseSync* pointer on the C++ stack, which
V8 GC does not track.
Add a BaseObjectPtr<DatabaseSync> guard that keeps the database
alive for the duration of these SQLite API calls, preventing a
use-after-free when the JavaScript callback triggers GC.
Signed-off-by: Matteo Collina <hello@matteocollina.com>
PR-URL: https://github.com/nodejs/node/pull/64535
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: René <contact.9a5d6388@renegade334.me.uk>
When sqlite3_open_v2() fails, SQLite still assigns a database handle
in a "sick" state. Such a handle may only be used to retrieve the
error and must then be released with sqlite3_close(). Keeping it in
connection_ meant that after a failed open():
- isOpen incorrectly reported true,
- every method passed the "database is not open" check and called
SQLite APIs on the sick handle, which is an API misuse,
- registering a user-defined function leaked its user data in builds
with SQLITE_ENABLE_API_ARMOR, because SQLite rejects the call
before taking ownership of the user data,
- open() could not be retried since the database appeared open.
Close and reset the connection handle when open() fails at any point
so that the database remains closed and open() can be retried.
Fixes: https://github.com/nodejs/node/issues/63831
Signed-off-by: Yagiz Nizipli <yagiz@nizipli.com>
PR-URL: https://github.com/nodejs/node/pull/63854
Reviewed-By: Matteo Collina <matteo.collina@gmail.com>
Reviewed-By: Filip Skokan <panva.ip@gmail.com>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
When using SQLite tag store functions (sql.get`, sql.run`, etc.),
preparation errors now show descriptive SQLite error messages instead
of the generic 'Failed to prepare statement' message.
The tag store's GetOrCreateStatement function now passes the database
object to THROW_ERR_SQLITE_ERROR, allowing it to retrieve the actual
error message from sqlite3_errmsg(), matching the behavior of the
regular db.prepare() method.
Fixes: https://github.com/nodejs/node/issues/61051
PR-URL: https://github.com/nodejs/node/pull/61096
Reviewed-By: Zeyu "Alex" Yang <himself65@outlook.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Edy Silva <edigleyssonsilva@gmail.com>
Reviewed-By: Luigi Pinca <luigipinca@gmail.com>