Version
v25+
Platform
all platforms are impacted
Subsystem
sqlite
What steps will reproduce the bug?
Per the SQLite docs for sqlite3_set_authorizer():
The authorizer callback must not do anything that will modify the database connection that invoked the authorizer callback.
The same paragraph says sqlite3_prepare_v2() and sqlite3_step() both count as modifying the connection.
node:sqlite documents database.setAuthorizer() as a wrapper around sqlite3_set_authorizer(), but it currently lets an authorizer callback call APIs that map to those operations on the same DatabaseSync connection:
const { DatabaseSync, constants } = require('node:sqlite');
const db = new DatabaseSync(':memory:');
db.exec('CREATE TABLE t (x INTEGER)');
let calls = 0;
let nestedPrepareSucceeded = false;
let nestedStepSucceeded = false;
db.setAuthorizer(() => {
if (++calls === 1) {
const stmt = db.prepare('SELECT 1'); // sqlite3_prepare_v2()
nestedPrepareSucceeded = true;
stmt.get(); // sqlite3_step()
nestedStepSucceeded = true;
}
return constants.SQLITE_OK;
});
db.prepare('SELECT x FROM t');
console.log({
calls,
nestedPrepareSucceeded,
nestedStepSucceeded,
});
How often does it reproduce? Is there a required condition?
Every time
What is the expected behavior? Why is that the expected behavior?
The nested same-connection db.prepare() should throw ERR_INVALID_STATE while the authorizer callback is on the stack.
This is not limited to user-facing prepare(). The SQLite docs also note:
When sqlite3_prepare_v2() is used to prepare a statement, the statement might be re-prepared during sqlite3_step() due to a schema change.
So the guard should cover every authorizer callback invocation, regardless of whether it was triggered by an explicit prepare or by re-prepare during step.
What do you see instead?
{
calls: 3,
nestedPrepareSucceeded: true,
nestedStepSucceeded: true
}
Additional information
APIs that should likely be rejected while a same-connection authorizer callback is on the stack include at least:
db.close()
db.exec()
db.prepare()
db.deserialize()
db.setAuthorizer()
stmt.run()
stmt.get()
stmt.all()
stmt.iterate()
iter.next()
iter.return()
sqlTagStore.run()
sqlTagStore.get()
sqlTagStore.all()
sqlTagStore.iterate()
sqlTagStore.clear()
There may be additional same-connection mutating APIs that should be included as well, such as APIs that alter connection configuration, registered functions, extensions, sessions, or limits.
This is related to the recent user-defined function callback reentry work, but authorizers need a separate guard. User-defined SQL functions are allowed to call other SQLite APIs as long as they do not close the connection or reset/finalize the running statement. Authorizer callbacks are stricter: SQLite says they must not do anything that modifies the invoking connection, and explicitly includes prepare and step in that definition.
Since node:sqlite is still Stability 1.2 / release candidate, I think this can probably be changed directly rather than going through a deprecation cycle. A doc note under database.setAuthorizer() would also be useful.
(disclaimer: found while working on #63180 and #63183 -- refined with the help of claude and codex)
Version
v25+
Platform
Subsystem
sqlite
What steps will reproduce the bug?
Per the SQLite docs for
sqlite3_set_authorizer():The same paragraph says
sqlite3_prepare_v2()andsqlite3_step()both count as modifying the connection.node:sqlitedocumentsdatabase.setAuthorizer()as a wrapper aroundsqlite3_set_authorizer(), but it currently lets an authorizer callback call APIs that map to those operations on the sameDatabaseSyncconnection:How often does it reproduce? Is there a required condition?
Every time
What is the expected behavior? Why is that the expected behavior?
The nested same-connection
db.prepare()should throwERR_INVALID_STATEwhile the authorizer callback is on the stack.This is not limited to user-facing
prepare(). The SQLite docs also note:So the guard should cover every authorizer callback invocation, regardless of whether it was triggered by an explicit prepare or by re-prepare during step.
What do you see instead?
Additional information
APIs that should likely be rejected while a same-connection authorizer callback is on the stack include at least:
There may be additional same-connection mutating APIs that should be included as well, such as APIs that alter connection configuration, registered functions, extensions, sessions, or limits.
This is related to the recent user-defined function callback reentry work, but authorizers need a separate guard. User-defined SQL functions are allowed to call other SQLite APIs as long as they do not close the connection or reset/finalize the running statement. Authorizer callbacks are stricter: SQLite says they must not do anything that modifies the invoking connection, and explicitly includes prepare and step in that definition.
Since
node:sqliteis still Stability 1.2 / release candidate, I think this can probably be changed directly rather than going through a deprecation cycle. A doc note underdatabase.setAuthorizer()would also be useful.(disclaimer: found while working on #63180 and #63183 -- refined with the help of claude and codex)