sqlite: reject connection access from authorizer callbacks

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>
This commit is contained in:
Trevor Burnham authored and GitHub committed 2026-08-14 21:43:35 +00:00
1 parent bc813a7a6d
commit c4b755ee00
5 files changed
+711 -7

No files matched your search

+49
View File
@@ -9,6 +9,7 @@
#include "sqlite3.h"
#include "util.h"
#include <algorithm>
#include <array>
#include <list>
#include <map>
@@ -265,6 +266,26 @@ class DatabaseSync : public BaseObject {
void DecrementCallbackDepth() { --callback_depth_; }
bool IsInCallback() const { return callback_depth_ > 0; }
// SQLite forbids an authorizer callback from doing anything that modifies
// the database connection that invoked it, which includes preparing and
// stepping statements. See https://www.sqlite.org/c3ref/set_authorizer.html.
void IncrementAuthorizerDepth() { ++authorizer_depth_; }
void DecrementAuthorizerDepth() { --authorizer_depth_; }
bool IsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
// Finalizing a statement frees its virtual machine, so a callback that
// SQLite invokes from inside sqlite3_step() must not finalize the statement
// being stepped. Other statements on the connection are safe to finalize.
void PushSteppingStatement(sqlite3_stmt* stmt) {
stepping_statements_.push_back(stmt);
}
void PopSteppingStatement() { stepping_statements_.pop_back(); }
bool IsSteppingStatement(sqlite3_stmt* stmt) const {
return std::find(stepping_statements_.begin(),
stepping_statements_.end(),
stmt) != stepping_statements_.end();
}
SET_MEMORY_INFO_NAME(DatabaseSync)
SET_SELF_SIZE(DatabaseSync)
@@ -279,6 +300,8 @@ class DatabaseSync : public BaseObject {
sqlite3* connection_;
bool ignore_next_sqlite_error_;
int callback_depth_ = 0;
int authorizer_depth_ = 0;
std::vector<sqlite3_stmt*> stepping_statements_;
std::set<BackupJob*> backups_;
std::unordered_set<Session*> sessions_;
@@ -460,6 +483,32 @@ class CallbackDepthGuard {
DatabaseSync* db_;
};
class SteppingStatementGuard {
public:
SteppingStatementGuard(DatabaseSync* db, sqlite3_stmt* stmt) : db_(db) {
db_->PushSteppingStatement(stmt);
}
~SteppingStatementGuard() { db_->PopSteppingStatement(); }
SteppingStatementGuard(const SteppingStatementGuard&) = delete;
SteppingStatementGuard& operator=(const SteppingStatementGuard&) = delete;
private:
DatabaseSync* db_;
};
class AuthorizerDepthGuard {
public:
explicit AuthorizerDepthGuard(DatabaseSync* db) : db_(db) {
db_->IncrementAuthorizerDepth();
}
~AuthorizerDepthGuard() { db_->DecrementAuthorizerDepth(); }
AuthorizerDepthGuard(const AuthorizerDepthGuard&) = delete;
AuthorizerDepthGuard& operator=(const AuthorizerDepthGuard&) = delete;
private:
DatabaseSync* db_;
};
class UserDefinedFunction {
public:
UserDefinedFunction(Environment* env,