mirror of
https://github.com/nodejs/node.git
synced 2026-11-09 15:11:11 -05:00
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:
1 parent
bc813a7a6d
commit
c4b755ee00
5 files changed
+711
-7
No files matched your search
@@ -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,
|
||||
|
||||
Reference in new issue
Block a user