Skip to content

Commit 2e55c6a

Browse files
louwersaduh95
authored andcommitted
sqlite: allow setting defensive flag
PR-URL: #60217 Reviewed-By: Colin Ihrig <[email protected]> Reviewed-By: Antoine du Hamel <[email protected]> Reviewed-By: Ulises Gascón <[email protected]>
1 parent 95644a4 commit 2e55c6a

File tree

5 files changed

+130
-0
lines changed

5 files changed

+130
-0
lines changed

doc/api/sqlite.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,10 @@ exposed by this class execute synchronously.
9898
<!-- YAML
9999
added: v22.5.0
100100
changes:
101+
- version:
102+
- REPLACEME
103+
pr-url: https://github.com/nodejs/node/pull/60217
104+
description: Add `defensive` option.
101105
- version:
102106
- v24.4.0
103107
- v22.18.0
@@ -140,6 +144,10 @@ changes:
140144
character (e.g., `foo` instead of `:foo`). **Default:** `true`.
141145
* `allowUnknownNamedParameters` {boolean} If `true`, unknown named parameters are ignored when binding.
142146
If `false`, an exception is thrown for unknown named parameters. **Default:** `false`.
147+
* `defensive` {boolean} If `true`, enables the defensive flag. When the defensive flag is enabled,
148+
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
149+
The defensive flag can also be set using `enableDefensive()`.
150+
**Default:** `false`.
143151

144152
Constructs a new `DatabaseSync` instance.
145153

@@ -261,6 +269,19 @@ Enables or disables the `loadExtension` SQL function, and the `loadExtension()`
261269
method. When `allowExtension` is `false` when constructing, you cannot enable
262270
loading extensions for security reasons.
263271

272+
### `database.enableDefensive(active)`
273+
274+
<!-- YAML
275+
added:
276+
- REPLACEME
277+
-->
278+
279+
* `active` {boolean} Whether to set the defensive flag.
280+
281+
Enables or disables the defensive flag. When the defensive flag is active,
282+
language features that allow ordinary SQL to deliberately corrupt the database file are disabled.
283+
See [`SQLITE_DBCONFIG_DEFENSIVE`][] in the SQLite documentation for details.
284+
264285
### `database.location([dbName])`
265286

266287
<!-- YAML
@@ -1306,6 +1327,7 @@ callback function to indicate what type of operation is being authorized.
13061327
[Type conversion between JavaScript and SQLite]: #type-conversion-between-javascript-and-sqlite
13071328
[`ATTACH DATABASE`]: https://www.sqlite.org/lang_attach.html
13081329
[`PRAGMA foreign_keys`]: https://www.sqlite.org/pragma.html#pragma_foreign_keys
1330+
[`SQLITE_DBCONFIG_DEFENSIVE`]: https://www.sqlite.org/c3ref/c_dbconfig_defensive.html#sqlitedbconfigdefensive
13091331
[`SQLITE_DETERMINISTIC`]: https://www.sqlite.org/c3ref/c_deterministic.html
13101332
[`SQLITE_DIRECTONLY`]: https://www.sqlite.org/c3ref/c_deterministic.html
13111333
[`SQLITE_MAX_FUNCTION_ARG`]: https://www.sqlite.org/limits.html#max_function_arg

src/env_properties.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@
130130
V(cwd_string, "cwd") \
131131
V(data_string, "data") \
132132
V(default_is_true_string, "defaultIsTrue") \
133+
V(defensive_string, "defensive") \
133134
V(deserialize_info_string, "deserializeInfo") \
134135
V(dest_string, "dest") \
135136
V(destroyed_string, "destroyed") \

src/node_sqlite.cc

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -753,6 +753,14 @@ bool DatabaseSync::Open() {
753753
CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false);
754754
CHECK_EQ(foreign_keys_enabled, open_config_.get_enable_foreign_keys());
755755

756+
int defensive_enabled;
757+
r = sqlite3_db_config(connection_,
758+
SQLITE_DBCONFIG_DEFENSIVE,
759+
static_cast<int>(open_config_.get_enable_defensive()),
760+
&defensive_enabled);
761+
CHECK_ERROR_OR_THROW(env()->isolate(), this, r, SQLITE_OK, false);
762+
CHECK_EQ(defensive_enabled, open_config_.get_enable_defensive());
763+
756764
sqlite3_busy_timeout(connection_, open_config_.get_timeout());
757765

758766
if (allow_load_extension_) {
@@ -1065,6 +1073,21 @@ void DatabaseSync::New(const FunctionCallbackInfo<Value>& args) {
10651073
allow_unknown_named_params_v.As<Boolean>()->Value());
10661074
}
10671075
}
1076+
1077+
Local<Value> defensive_v;
1078+
if (!options->Get(env->context(), env->defensive_string())
1079+
.ToLocal(&defensive_v)) {
1080+
return;
1081+
}
1082+
if (!defensive_v->IsUndefined()) {
1083+
if (!defensive_v->IsBoolean()) {
1084+
THROW_ERR_INVALID_ARG_TYPE(
1085+
env->isolate(),
1086+
"The \"options.defensive\" argument must be a boolean.");
1087+
return;
1088+
}
1089+
open_config.set_enable_defensive(defensive_v.As<Boolean>()->Value());
1090+
}
10681091
}
10691092

10701093
new DatabaseSync(
@@ -1835,6 +1858,26 @@ void DatabaseSync::EnableLoadExtension(
18351858
CHECK_ERROR_OR_THROW(isolate, db, load_extension_ret, SQLITE_OK, void());
18361859
}
18371860

1861+
void DatabaseSync::EnableDefensive(const FunctionCallbackInfo<Value>& args) {
1862+
DatabaseSync* db;
1863+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
1864+
Environment* env = Environment::GetCurrent(args);
1865+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1866+
1867+
auto isolate = args.GetIsolate();
1868+
if (!args[0]->IsBoolean()) {
1869+
THROW_ERR_INVALID_ARG_TYPE(isolate,
1870+
"The \"active\" argument must be a boolean.");
1871+
return;
1872+
}
1873+
1874+
const int enable = args[0].As<Boolean>()->Value();
1875+
int defensive_enabled;
1876+
const int defensive_ret = sqlite3_db_config(
1877+
db->connection_, SQLITE_DBCONFIG_DEFENSIVE, enable, &defensive_enabled);
1878+
CHECK_ERROR_OR_THROW(isolate, db, defensive_ret, SQLITE_OK, void());
1879+
}
1880+
18381881
void DatabaseSync::LoadExtension(const FunctionCallbackInfo<Value>& args) {
18391882
DatabaseSync* db;
18401883
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
@@ -3316,6 +3359,8 @@ static void Initialize(Local<Object> target,
33163359
db_tmpl,
33173360
"enableLoadExtension",
33183361
DatabaseSync::EnableLoadExtension);
3362+
SetProtoMethod(
3363+
isolate, db_tmpl, "enableDefensive", DatabaseSync::EnableDefensive);
33193364
SetProtoMethod(
33203365
isolate, db_tmpl, "loadExtension", DatabaseSync::LoadExtension);
33213366
SetProtoMethod(

src/node_sqlite.h

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ class DatabaseOpenConfiguration {
6565
return allow_unknown_named_params_;
6666
}
6767

68+
inline void set_enable_defensive(bool flag) { defensive_ = flag; }
69+
70+
inline bool get_enable_defensive() const { return defensive_; }
71+
6872
private:
6973
std::string location_;
7074
bool read_only_ = false;
@@ -75,6 +79,7 @@ class DatabaseOpenConfiguration {
7579
bool return_arrays_ = false;
7680
bool allow_bare_named_params_ = true;
7781
bool allow_unknown_named_params_ = false;
82+
bool defensive_ = false;
7883
};
7984

8085
class DatabaseSync;
@@ -140,6 +145,7 @@ class DatabaseSync : public BaseObject {
140145
static void ApplyChangeset(const v8::FunctionCallbackInfo<v8::Value>& args);
141146
static void EnableLoadExtension(
142147
const v8::FunctionCallbackInfo<v8::Value>& args);
148+
static void EnableDefensive(const v8::FunctionCallbackInfo<v8::Value>& args);
143149
static void LoadExtension(const v8::FunctionCallbackInfo<v8::Value>& args);
144150
static void SetAuthorizer(const v8::FunctionCallbackInfo<v8::Value>& args);
145151
static int AuthorizerCallback(void* user_data,
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
'use strict';
2+
const { skipIfSQLiteMissing } = require('../common/index.mjs');
3+
const { test } = require('node:test');
4+
const assert = require('node:assert');
5+
const { DatabaseSync } = require('node:sqlite');
6+
skipIfSQLiteMissing();
7+
8+
function checkDefensiveMode(db) {
9+
function journalMode() {
10+
return db.prepare('PRAGMA journal_mode').get().journal_mode;
11+
}
12+
13+
assert.strictEqual(journalMode(), 'memory');
14+
db.exec('PRAGMA journal_mode=OFF');
15+
16+
switch (journalMode()) {
17+
case 'memory': return true; // journal_mode unchanged, defensive mode must be active
18+
case 'off': return false; // journal_mode now 'off', so defensive mode not active
19+
default: throw new Error('unexpected journal_mode');
20+
}
21+
}
22+
23+
test('by default, defensive mode is off', (t) => {
24+
const db = new DatabaseSync(':memory:');
25+
t.assert.strictEqual(checkDefensiveMode(db), false);
26+
});
27+
28+
test('when passing { defensive: true } as config, defensive mode is on', (t) => {
29+
const db = new DatabaseSync(':memory:', {
30+
defensive: true
31+
});
32+
t.assert.strictEqual(checkDefensiveMode(db), true);
33+
});
34+
35+
test('defensive mode on after calling db.enableDefensive(true)', (t) => {
36+
const db = new DatabaseSync(':memory:');
37+
db.enableDefensive(true);
38+
t.assert.strictEqual(checkDefensiveMode(db), true);
39+
});
40+
41+
test('defensive mode should be off after calling db.enableDefensive(false)', (t) => {
42+
const db = new DatabaseSync(':memory:', {
43+
defensive: true
44+
});
45+
db.enableDefensive(false);
46+
t.assert.strictEqual(checkDefensiveMode(db), false);
47+
});
48+
49+
test('throws if options.defensive is provided but is not a boolean', (t) => {
50+
t.assert.throws(() => {
51+
new DatabaseSync(':memory:', { defensive: 42 });
52+
}, {
53+
code: 'ERR_INVALID_ARG_TYPE',
54+
message: 'The "options.defensive" argument must be a boolean.',
55+
});
56+
});

0 commit comments

Comments
 (0)