Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions doc/api/sqlite.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.

Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -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") \
Expand Down
45 changes: 45 additions & 0 deletions src/node_sqlite.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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_) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -1837,6 +1860,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());
Expand Down Expand Up @@ -3318,6 +3361,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(
Expand Down
6 changes: 6 additions & 0 deletions src/node_sqlite.h
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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,
Expand Down
56 changes: 56 additions & 0 deletions test/parallel/test-sqlite-config.js
Original file line number Diff line number Diff line change
@@ -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.',
});
});
Loading