sqlite: allow setting defensive flag

PR-URL: https://github.com/nodejs/node/pull/60217
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
Reviewed-By: Antoine du Hamel <duhamelantoine1995@gmail.com>
Reviewed-By: Ulises Gascón <ulisesgascongonzalez@gmail.com>
This commit is contained in:
Bart Louwers 2025-10-27 15:01:16 +01:00 committed by GitHub
parent 1f2c9f82b7
commit fd7b33e763
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 130 additions and 0 deletions

View File

@ -98,6 +98,10 @@ exposed by this class execute synchronously.
<!-- YAML
added: v22.5.0
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/60217
description: Add `defensive` option.
- version:
- v24.4.0
- v22.18.0
@ -140,6 +144,10 @@ changes:
character (e.g., `foo` instead of `:foo`). **Default:** `true`.
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters are ignored when binding.
If `false`, an exception is thrown for unknown named parameters. **Default:** `false`.
* `defensive` {boolean} If `true`, enables the defensive flag. When the defensive flag is enabled,
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
The defensive flag can also be set using `enableDefensive()`.
**Default:** `false`.
Constructs a new `DatabaseSync` instance.
@ -261,6 +269,19 @@ Enables or disables the `loadExtension` SQL function, and the `loadExtension()`
method. When `allowExtension` is `false` when constructing, you cannot enable
loading extensions for security reasons.
### `database.enableDefensive(active)`
<!-- YAML
added:
- REPLACEME
-->
* `active` {boolean} Whether to set the defensive flag.
Enables or disables the defensive flag. When the defensive flag is active,
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
See [`SQLITE_DBCONFIG_DEFENSIVE`][] in the SQLite documentation for details.
### `database.location([dbName])`
<!-- YAML
@ -1306,6 +1327,7 @@ callback function to indicate what type of operation is being authorized.
[Type conversion between JavaScript and SQLite]: #type-conversion-between-javascript-and-sqlite
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
[`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys
[`SQLITE_DBCONFIG_DEFENSIVE`]: https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigdefensive
[`SQLITE_DETERMINISTIC`]: https://www.sqlite.org/c3ref/c_deterministic.html
[`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html
[`SQLITE_MAX_FUNCTION_ARG`]: https://www.sqlite.org/limits.html#max_function_arg

View File

@ -130,6 +130,7 @@
V(cwd_string, "cwd") \
V(data_string, "data") \
V(default_is_true_string, "defaultIsTrue") \
V(defensive_string, "defensive") \
V(deserialize_info_string, "deserializeInfo") \
V(dest_string, "dest") \
V(destroyed_string, "destroyed") \

View File

@ -753,6 +753,14 @@ bool DatabaseSync::Open() {
CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false);
CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys());
int defensive_enabled;
r = sqlite3_db_config(connection_,
SQLITE_DBCONFIG_DEFENSIVE,
static_cast<int>(open_config_.get_enable_defensive()),
&defensive_enabled);
CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false);
CHECK_EQ(defensive_enabled, open_config_.get_enable_defensive());
sqlite3_busy_timeout(connection_, open_config_.get_timeout());
if (allow_load_extension_) {
@ -1065,6 +1073,21 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
allow_unknown_named_params_v.As<Boolean>()->Value());
}
}
Local<Value> defensive_v;
if (!options->Get(env->context(), env->defensive_string())
.ToLocal(&defensive_v)) {
return;
}
if (!defensive_v->IsUndefined()) {
if (!defensive_v->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(
env->isolate(),
"The \"options.defensive\" argument must be a boolean.");
return;
}
open_config.set_enable_defensive(defensive_v.As<Boolean>()->Value());
}
}
new DatabaseSync(
@ -1835,6 +1858,26 @@ void DatabaseSync::EnableLoadExtension(
CHECK_ERROR_OR_THROW(isolate, db, load_extension_ret, SQLITE_OK, void());
}
void DatabaseSync::EnableDefensive(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
Environment* env = Environment::GetCurrent(args);
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
auto isolate = args.GetIsolate();
if (!args[0]->IsBoolean()) {
THROW_ERR_INVALID_ARG_TYPE(isolate,
"The \"active\" argument must be a boolean.");
return;
}
const int enable = args[0].As<Boolean>()->Value();
int defensive_enabled;
const int defensive_ret = sqlite3_db_config(
db->connection_, SQLITE_DBCONFIG_DEFENSIVE, enable, &defensive_enabled);
CHECK_ERROR_OR_THROW(isolate, db, defensive_ret, SQLITE_OK, void());
}
void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
DatabaseSync* db;
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
@ -3316,6 +3359,8 @@ static void Initialize(Local<Object> target,
db_tmpl,
"enableLoadExtension",
DatabaseSync::EnableLoadExtension);
SetProtoMethod(
isolate, db_tmpl, "enableDefensive", DatabaseSync::EnableDefensive);
SetProtoMethod(
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
SetProtoMethod(

View File

@ -65,6 +65,10 @@ class DatabaseOpenConfiguration {
return allow_unknown_named_params_;
}
inline void set_enable_defensive(bool flag) { defensive_ = flag; }
inline bool get_enable_defensive() const { return defensive_; }
private:
std::string location_;
bool read_only_ = false;
@ -75,6 +79,7 @@ class DatabaseOpenConfiguration {
bool return_arrays_ = false;
bool allow_bare_named_params_ = true;
bool allow_unknown_named_params_ = false;
bool defensive_ = false;
};
class DatabaseSync;
@ -140,6 +145,7 @@ class DatabaseSync : public BaseObject {
static void ApplyChangeset(const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableLoadExtension(
const v8::FunctionCallbackInfo<v8::Value>& args);
static void EnableDefensive(const v8::FunctionCallbackInfo<v8::Value>& args);
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
static void SetAuthorizer(const v8::FunctionCallbackInfo<v8::Value>& args);
static int AuthorizerCallback(void* user_data,

View File

@ -0,0 +1,56 @@
'use strict';
const { skipIfSQLiteMissing } = require('../common/index.mjs');
const { test } = require('node:test');
const assert = require('node:assert');
const { DatabaseSync } = require('node:sqlite');
skipIfSQLiteMissing();
function checkDefensiveMode(db) {
function journalMode() {
return db.prepare('PRAGMA journal_mode').get().journal_mode;
}
assert.strictEqual(journalMode(), 'memory');
db.exec('PRAGMA journal_mode=OFF');
switch (journalMode()) {
case 'memory': return true; // journal_mode unchanged, defensive mode must be active
case 'off': return false; // journal_mode now 'off', so defensive mode not active
default: throw new Error('unexpected journal_mode');
}
}
test('by default, defensive mode is off', (t) => {
const db = new DatabaseSync(':memory:');
t.assert.strictEqual(checkDefensiveMode(db), false);
});
test('when passing { defensive: true } as config, defensive mode is on', (t) => {
const db = new DatabaseSync(':memory:', {
defensive: true
});
t.assert.strictEqual(checkDefensiveMode(db), true);
});
test('defensive mode on after calling db.enableDefensive(true)', (t) => {
const db = new DatabaseSync(':memory:');
db.enableDefensive(true);
t.assert.strictEqual(checkDefensiveMode(db), true);
});
test('defensive mode should be off after calling db.enableDefensive(false)', (t) => {
const db = new DatabaseSync(':memory:', {
defensive: true
});
db.enableDefensive(false);
t.assert.strictEqual(checkDefensiveMode(db), false);
});
test('throws if options.defensive is provided but is not a boolean', (t) => {
t.assert.throws(() => {
new DatabaseSync(':memory:', { defensive: 42 });
}, {
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "options.defensive" argument must be a boolean.',
});
});