Skip to content

Commit 3536092

Browse files
committed
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 an authorizer 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 the callback is on the stack. The depth is per-connection, so other connections stay usable from the callback. The guard covers every authorizer invocation, not just those from an explicit prepare(), since SQLite may re-prepare a statement during sqlite3_step() after a schema change. serialize() and the session changeset() and patchset() methods prepare statements internally, so they re-enter the authorizer too. Reentry through changeset() does not terminate: it recurses until the process is killed, with no way to catch it from JavaScript. Finalizing a statement is a separate hazard. It frees the virtual machine that the enclosing sqlite3_step() is still executing, which crashes rather than throwing, and any callback SQLite invokes during execution can reach it. statement.close() therefore rejects while any callback is on the stack, not just an authorizer, so the equivalent crash through a user-defined function is fixed as well. Disposal stays idempotent, since throwing for an already-finalized statement would demote a `using` scope's exception to a SuppressedError. Signed-off-by: Trevor Burnham <trevorburnham@gmail.com> Fixes: #63207 Assisted-by: claude:opus-5
1 parent e6fa5bf commit 3536092

5 files changed

Lines changed: 371 additions & 2 deletions

File tree

doc/api/sqlite.md

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -439,6 +439,11 @@ wrapper around [`sqlite3_create_function_v2()`][].
439439

440440
<!-- YAML
441441
added: v24.10.0
442+
changes:
443+
- version: REPLACEME
444+
pr-url: https://github.com/nodejs/node/pull/65156
445+
description: Accessing the invoking database connection from the authorizer
446+
callback now throws.
442447
-->
443448

444449
* `callback` {Function|null} The authorizer function to set, or `null` to
@@ -464,6 +469,21 @@ The callback must return one of the following constants:
464469
* `SQLITE_DENY` - Deny the operation (causes an error).
465470
* `SQLITE_IGNORE` - Ignore the operation (silently skip).
466471

472+
SQLite requires that the authorizer callback not modify the database connection
473+
that invoked it, which includes preparing and stepping statements. Methods that
474+
would do so throw an error with code `ERR_INVALID_STATE` while the callback is
475+
on the stack, including `database.prepare()`, `database.exec()`, the execution
476+
methods of that connection's statements, iterators, and tag stores, and
477+
`database.setAuthorizer()` itself. Other connections remain usable.
478+
479+
The callback can also be invoked from within `statement.run()`,
480+
`statement.get()`, and similar methods, because SQLite may re-prepare a
481+
statement during execution after a schema change.
482+
483+
Separately, `statement.close()` throws if called from any callback SQLite
484+
invokes during execution, such as a user-defined function, because finalizing a
485+
statement that is mid-execution would free the virtual machine that is running.
486+
467487
```cjs
468488
const { DatabaseSync, constants } = require('node:sqlite');
469489
const db = new DatabaseSync(':memory:');

src/node_sqlite.cc

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,24 @@ inline MaybeLocal<String> Utf8StringMaybeOneByte(Isolate* isolate,
9696
} \
9797
} while (0)
9898

99+
// SQLite requires that an authorizer callback not modify the connection that
100+
// invoked it. Preparing and stepping statements both count as modifying it.
101+
// See https://www.sqlite.org/c3ref/set_authorizer.html.
102+
#define THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db) \
103+
THROW_AND_RETURN_ON_BAD_STATE( \
104+
(env), \
105+
(db)->IsInAuthorizerCallback(), \
106+
"database cannot be accessed from an authorizer callback")
107+
108+
// Finalizing a statement frees its virtual machine. Any callback that SQLite
109+
// invokes from inside sqlite3_step() may be running on that very statement, so
110+
// finalizing from one is a use-after-free rather than a contract violation.
111+
#define THROW_AND_RETURN_IF_IN_CALLBACK(env, db) \
112+
THROW_AND_RETURN_ON_BAD_STATE( \
113+
(env), \
114+
(db)->IsInCallback(), \
115+
"statement cannot be finalized from a callback")
116+
99117
#define SQLITE_VALUE_TO_JS(from, isolate, use_big_int_args, result, ...) \
100118
do { \
101119
switch (sqlite3_##from##_type(__VA_ARGS__)) { \
@@ -825,6 +843,12 @@ Intercepted DatabaseSyncLimits::LimitsSetter(
825843
return Intercepted::kYes;
826844
}
827845

846+
if (limits->database_->IsInAuthorizerCallback()) {
847+
THROW_ERR_INVALID_STATE(
848+
env, "database cannot be accessed from an authorizer callback");
849+
return Intercepted::kYes;
850+
}
851+
828852
if (!value->IsNumber()) {
829853
THROW_ERR_INVALID_ARG_TYPE(
830854
isolate, "Limit value must be a non-negative integer or Infinity.");
@@ -1081,6 +1105,7 @@ void DatabaseSync::CreateTagStore(const FunctionCallbackInfo<Value>& args) {
10811105
THROW_ERR_INVALID_STATE(env, "database is not open");
10821106
return;
10831107
}
1108+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
10841109
int capacity = 1000;
10851110
if (args.Length() > 0 && !args[0]->IsUndefined()) {
10861111
if (!args[0]->IsNumber()) {
@@ -1483,6 +1508,7 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
14831508
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
14841509
Environment* env = Environment::GetCurrent(args);
14851510
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1511+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
14861512

14871513
if (!args[0]->IsString()) {
14881514
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -1606,6 +1632,7 @@ void DatabaseSync::Exec(const FunctionCallbackInfo<Value>& args) {
16061632
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
16071633
Environment* env = Environment::GetCurrent(args);
16081634
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1635+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
16091636

16101637
if (!args[0]->IsString()) {
16111638
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -1630,6 +1657,7 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo<Value>& args) {
16301657
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
16311658
Environment* env = Environment::GetCurrent(args);
16321659
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1660+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
16331661

16341662
if (!args[0]->IsString()) {
16351663
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -1803,6 +1831,7 @@ void DatabaseSync::Serialize(const FunctionCallbackInfo<Value>& args) {
18031831
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
18041832
Environment* env = Environment::GetCurrent(args);
18051833
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1834+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
18061835

18071836
std::string db_name = "main";
18081837
if (!args[0]->IsUndefined()) {
@@ -1858,6 +1887,7 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
18581887
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
18591888
Environment* env = Environment::GetCurrent(args);
18601889
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1890+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
18611891

18621892
if (!args[0]->IsUint8Array()) {
18631893
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -1933,6 +1963,7 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
19331963
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
19341964
Environment* env = Environment::GetCurrent(args);
19351965
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1966+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
19361967
Utf8Value name(env->isolate(), args[0].As<String>());
19371968
Local<Object> options = args[1].As<Object>();
19381969
Local<Value> start_v;
@@ -2144,6 +2175,7 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
21442175
DatabaseSync* db;
21452176
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
21462177
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2178+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
21472179

21482180
sqlite3_session* pSession;
21492181
int r = sqlite3session_create(db->connection_, db_name.c_str(), &pSession);
@@ -2313,6 +2345,7 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
23132345
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
23142346
Environment* env = Environment::GetCurrent(args);
23152347
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2348+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
23162349

23172350
if (!args[0]->IsUint8Array()) {
23182351
THROW_ERR_INVALID_ARG_TYPE(
@@ -2447,6 +2480,7 @@ void DatabaseSync::EnableLoadExtension(
24472480
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
24482481
Environment* env = Environment::GetCurrent(args);
24492482
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2483+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
24502484

24512485
Isolate* isolate = env->isolate();
24522486
if (!args[0]->IsBoolean()) {
@@ -2475,6 +2509,7 @@ void DatabaseSync::EnableDefensive(const FunctionCallbackInfo<Value>& args) {
24752509
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
24762510
Environment* env = Environment::GetCurrent(args);
24772511
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2512+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
24782513

24792514
Isolate* isolate = env->isolate();
24802515
if (!args[0]->IsBoolean()) {
@@ -2500,6 +2535,7 @@ void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
25002535
env, !db->allow_load_extension_, "extension loading is not allowed");
25012536
THROW_AND_RETURN_ON_BAD_STATE(
25022537
env, !db->enable_load_extension_, "extension loading is not allowed");
2538+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
25032539

25042540
if (!args[0]->IsString()) {
25052541
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -2528,6 +2564,7 @@ void DatabaseSync::SetAuthorizer(const FunctionCallbackInfo<Value>& args) {
25282564
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
25292565
Environment* env = Environment::GetCurrent(args);
25302566
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2567+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
25312568

25322569
Isolate* isolate = env->isolate();
25332570

@@ -2564,6 +2601,7 @@ int DatabaseSync::AuthorizerCallback(void* user_data,
25642601
const char* param4) {
25652602
DatabaseSync* db = static_cast<DatabaseSync*>(user_data);
25662603
CallbackDepthGuard guard(db);
2604+
AuthorizerDepthGuard authorizer_guard(db);
25672605
Environment* env = db->env();
25682606
Isolate* isolate = env->isolate();
25692607
HandleScope handle_scope(isolate);
@@ -2677,12 +2715,20 @@ void StatementSync::Close(const FunctionCallbackInfo<Value>& args) {
26772715
Environment* env = Environment::GetCurrent(args);
26782716
THROW_AND_RETURN_ON_BAD_STATE(
26792717
env, stmt->IsFinalized(), "statement has been finalized");
2718+
THROW_AND_RETURN_IF_IN_CALLBACK(env, stmt->db_.get());
26802719
stmt->Close();
26812720
}
26822721

26832722
void StatementSync::Dispose(const FunctionCallbackInfo<Value>& args) {
26842723
StatementSync* stmt;
26852724
ASSIGN_OR_RETURN_UNWRAP(&stmt, args.This());
2725+
Environment* env = Environment::GetCurrent(args);
2726+
// Disposal is idempotent, so an already-finalized statement is a no-op even
2727+
// inside a callback.
2728+
if (stmt->IsFinalized()) {
2729+
return;
2730+
}
2731+
THROW_AND_RETURN_IF_IN_CALLBACK(env, stmt->db_.get());
26862732
stmt->Close();
26872733
}
26882734

@@ -3127,6 +3173,7 @@ void StatementSync::All(const FunctionCallbackInfo<Value>& args) {
31273173
Environment* env = Environment::GetCurrent(args);
31283174
THROW_AND_RETURN_ON_BAD_STATE(
31293175
env, stmt->IsFinalized(), "statement has been finalized");
3176+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
31303177
Isolate* isolate = env->isolate();
31313178
int r = stmt->ResetStatement();
31323179
CHECK_ERROR_OR_THROW(isolate, stmt->db_.get(), r, SQLITE_OK, void());
@@ -3154,6 +3201,7 @@ void StatementSync::Iterate(const FunctionCallbackInfo<Value>& args) {
31543201
Environment* env = Environment::GetCurrent(args);
31553202
THROW_AND_RETURN_ON_BAD_STATE(
31563203
env, stmt->IsFinalized(), "statement has been finalized");
3204+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
31573205
int r = stmt->ResetStatement();
31583206
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
31593207

@@ -3177,6 +3225,7 @@ void StatementSync::Get(const FunctionCallbackInfo<Value>& args) {
31773225
Environment* env = Environment::GetCurrent(args);
31783226
THROW_AND_RETURN_ON_BAD_STATE(
31793227
env, stmt->IsFinalized(), "statement has been finalized");
3228+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
31803229
int r = stmt->ResetStatement();
31813230
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
31823231

@@ -3201,6 +3250,7 @@ void StatementSync::Run(const FunctionCallbackInfo<Value>& args) {
32013250
Environment* env = Environment::GetCurrent(args);
32023251
THROW_AND_RETURN_ON_BAD_STATE(
32033252
env, stmt->IsFinalized(), "statement has been finalized");
3253+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, stmt->db_.get());
32043254
int r = stmt->ResetStatement();
32053255
CHECK_ERROR_OR_THROW(env->isolate(), stmt->db_.get(), r, SQLITE_OK, void());
32063256

@@ -3483,6 +3533,7 @@ void SQLTagStore::Run(const FunctionCallbackInfo<Value>& args) {
34833533

34843534
THROW_AND_RETURN_ON_BAD_STATE(
34853535
env, !session->database_->IsOpen(), "database is not open");
3536+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
34863537

34873538
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
34883539

@@ -3509,6 +3560,7 @@ void SQLTagStore::Iterate(const FunctionCallbackInfo<Value>& args) {
35093560

35103561
THROW_AND_RETURN_ON_BAD_STATE(
35113562
env, !session->database_->IsOpen(), "database is not open");
3563+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
35123564

35133565
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
35143566

@@ -3537,6 +3589,7 @@ void SQLTagStore::Get(const FunctionCallbackInfo<Value>& args) {
35373589

35383590
THROW_AND_RETURN_ON_BAD_STATE(
35393591
env, !session->database_->IsOpen(), "database is not open");
3592+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
35403593

35413594
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
35423595

@@ -3566,6 +3619,7 @@ void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
35663619

35673620
THROW_AND_RETURN_ON_BAD_STATE(
35683621
env, !session->database_->IsOpen(), "database is not open");
3622+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
35693623

35703624
BaseObjectPtr<StatementSync> stmt = PrepareStatement(args);
35713625

@@ -3592,6 +3646,10 @@ void SQLTagStore::All(const FunctionCallbackInfo<Value>& args) {
35923646
void SQLTagStore::Clear(const FunctionCallbackInfo<Value>& args) {
35933647
SQLTagStore* store;
35943648
ASSIGN_OR_RETURN_UNWRAP(&store, args.This());
3649+
Environment* env = Environment::GetCurrent(args);
3650+
if (store->database_) {
3651+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, store->database_.get());
3652+
}
35953653
store->sql_tags_.Clear();
35963654
}
35973655

@@ -3785,6 +3843,7 @@ void StatementSyncIterator::Next(const FunctionCallbackInfo<Value>& args) {
37853843
Environment* env = Environment::GetCurrent(args);
37863844
THROW_AND_RETURN_ON_BAD_STATE(
37873845
env, iter->stmt_->IsFinalized(), "statement has been finalized");
3846+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, iter->stmt_->db_.get());
37883847
Isolate* isolate = env->isolate();
37893848

37903849
auto iter_template = getLazyIterTemplate(env);
@@ -3862,6 +3921,7 @@ void StatementSyncIterator::Return(const FunctionCallbackInfo<Value>& args) {
38623921
Environment* env = Environment::GetCurrent(args);
38633922
THROW_AND_RETURN_ON_BAD_STATE(
38643923
env, iter->stmt_->IsFinalized(), "statement has been finalized");
3924+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, iter->stmt_->db_.get());
38653925
Isolate* isolate = env->isolate();
38663926

38673927
sqlite3_reset(iter->stmt_->statement_);
@@ -3940,6 +4000,7 @@ void Session::Changeset(const FunctionCallbackInfo<Value>& args) {
39404000
env, !session->database_->IsOpen(), "database is not open");
39414001
THROW_AND_RETURN_ON_BAD_STATE(
39424002
env, session->session_ == nullptr, "session is not open");
4003+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, session->database_.get());
39434004

39444005
int nChangeset;
39454006
void* pChangeset;

src/node_sqlite.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,13 @@ class DatabaseSync : public BaseObject {
233233
void DecrementCallbackDepth() { --callback_depth_; }
234234
bool IsInCallback() const { return callback_depth_ > 0; }
235235

236+
// SQLite forbids an authorizer callback from doing anything that modifies
237+
// the database connection that invoked it, which includes preparing and
238+
// stepping statements. See https://www.sqlite.org/c3ref/set_authorizer.html.
239+
void IncrementAuthorizerDepth() { ++authorizer_depth_; }
240+
void DecrementAuthorizerDepth() { --authorizer_depth_; }
241+
bool IsInAuthorizerCallback() const { return authorizer_depth_ > 0; }
242+
236243
SET_MEMORY_INFO_NAME(DatabaseSync)
237244
SET_SELF_SIZE(DatabaseSync)
238245

@@ -247,6 +254,7 @@ class DatabaseSync : public BaseObject {
247254
sqlite3* connection_;
248255
bool ignore_next_sqlite_error_;
249256
int callback_depth_ = 0;
257+
int authorizer_depth_ = 0;
250258

251259
std::set<BackupJob*> backups_;
252260
std::unordered_set<Session*> sessions_;
@@ -426,6 +434,19 @@ class CallbackDepthGuard {
426434
DatabaseSync* db_;
427435
};
428436

437+
class AuthorizerDepthGuard {
438+
public:
439+
explicit AuthorizerDepthGuard(DatabaseSync* db) : db_(db) {
440+
db_->IncrementAuthorizerDepth();
441+
}
442+
~AuthorizerDepthGuard() { db_->DecrementAuthorizerDepth(); }
443+
AuthorizerDepthGuard(const AuthorizerDepthGuard&) = delete;
444+
AuthorizerDepthGuard& operator=(const AuthorizerDepthGuard&) = delete;
445+
446+
private:
447+
DatabaseSync* db_;
448+
};
449+
429450
class UserDefinedFunction {
430451
public:
431452
UserDefinedFunction(Environment* env,

0 commit comments

Comments
 (0)