Skip to content

Commit 9cc829a

Browse files
TrevorBurnhamaduh95
authored andcommitted
sqlite: re-validate database state after reading options
prepare(), function(), aggregate(), deserialize(), applyChangeset() and backup() validated the connection, then read their options bag with Object::Get(). A property getter runs arbitrary JavaScript at that point, so a getter calling close() invalidates what was just checked. Five of the six then passed a null sqlite3* to SQLite and crashed; prepare() reported a spurious "out of memory". Re-check IsOpen() after option parsing, immediately before the SQLite call, keeping the early check so invalid calls still fail before any user code runs. IsOpen() is the only condition a getter can change: authorizer and callback depths are RAII-managed. createSession() already parsed options first, so it only gains the early check. deserialize() also latched the buffer length before reading options.dbName. A getter that shrank the backing store left the length too large; CopyContents() then handed the uninitialized remainder to SQLite, from where serialize() returned it to JavaScript. Check the CopyContents() result instead of discarding it. function() and aggregate() cast the callback's length property with As<Int32>() and no IsInt32() guard. length is configurable, so any type reached the cast and produced a silently wrong arity. Fixes: #65586 Signed-off-by: Trevor Burnham <trevorburnham@gmail.com> PR-URL: #65595 Reviewed-By: Trivikram Kamat <trivikr.dev@gmail.com>
1 parent bb4d42c commit 9cc829a

3 files changed

Lines changed: 379 additions & 11 deletions

File tree

doc/api/sqlite.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -241,7 +241,8 @@ Registers a new aggregate function with the SQLite database. This method is a wr
241241
JavaScript numbers. **Default:** `false`.
242242
* `varargs` {boolean} If `true`, `options.step` and `options.inverse` may be invoked with any number of
243243
arguments (between zero and [`SQLITE_MAX_FUNCTION_ARG`][]). If `false`,
244-
`inverse` and `step` must be invoked with exactly `length` arguments.
244+
`inverse` and `step` must be invoked with exactly `length` arguments, and
245+
their `length` properties must be integers.
245246
**Default:** `false`.
246247
* `start` {number | string | null | Array | Object | Function} The identity
247248
value for the aggregation function. This value is used when the aggregation
@@ -428,7 +429,8 @@ added:
428429
JavaScript numbers. **Default:** `false`.
429430
* `varargs` {boolean} If `true`, `function` may be invoked with any number of
430431
arguments (between zero and [`SQLITE_MAX_FUNCTION_ARG`][]). If `false`,
431-
`function` must be invoked with exactly `function.length` arguments.
432+
`function` must be invoked with exactly `function.length` arguments, which
433+
must be an integer.
432434
**Default:** `false`.
433435
* `fn` {Function} The JavaScript function to call when the SQLite function is
434436
invoked. The return value of this function should be a valid SQLite data type:

src/node_sqlite.cc

Lines changed: 72 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1742,6 +1742,10 @@ void DatabaseSync::Prepare(const FunctionCallbackInfo<Value>& args) {
17421742
}
17431743
}
17441744

1745+
// Reading the options bag above can run user JavaScript through a property
1746+
// getter, which may have closed the database since it was checked.
1747+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1748+
17451749
Utf8Value sql(env->isolate(), args[0].As<String>());
17461750
sqlite3_stmt* s = nullptr;
17471751

@@ -1929,9 +1933,22 @@ void DatabaseSync::CustomFunction(const FunctionCallbackInfo<Value>& args) {
19291933
if (!fn->Get(env->context(), env->length_string()).ToLocal(&js_len)) {
19301934
return;
19311935
}
1936+
1937+
if (!js_len->IsInt32()) {
1938+
THROW_ERR_INVALID_ARG_TYPE(
1939+
env->isolate(),
1940+
"The \"function.length\" property must be an integer.");
1941+
return;
1942+
}
1943+
19321944
argc = js_len.As<Int32>()->Value();
19331945
}
19341946

1947+
// Reading the options bag and "function.length" above can run user
1948+
// JavaScript through a property getter, which may have closed the database
1949+
// since it was checked.
1950+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
1951+
19351952
UserDefinedFunction* user_data = new UserDefinedFunction(
19361953
env, fn, BaseObjectWeakPtr<DatabaseSync>(db), use_bigint_args);
19371954
int text_rep = SQLITE_UTF8;
@@ -2094,6 +2111,10 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
20942111
}
20952112
}
20962113

2114+
// Reading the options bag above can run user JavaScript through a property
2115+
// getter, which may have closed the database since it was checked.
2116+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2117+
20972118
// sqlite3_malloc64 is required because SQLITE_DESERIALIZE_FREEONCLOSE
20982119
// transfers ownership to SQLite, which calls sqlite3_free() on close.
20992120
// See: https://www.sqlite.org/c3ref/deserialize.html
@@ -2104,7 +2125,16 @@ void DatabaseSync::Deserialize(const FunctionCallbackInfo<Value>& args) {
21042125
return;
21052126
}
21062127

2107-
input->CopyContents(buf, byte_length);
2128+
// The same user JavaScript may also have shrunk or detached the backing
2129+
// store, in which case byte_length is stale and CopyContents() leaves the
2130+
// remainder of buf uninitialized. Handing that to SQLite would disclose it
2131+
// through serialize().
2132+
if (input->CopyContents(buf, byte_length) != byte_length) {
2133+
sqlite3_free(buf);
2134+
THROW_ERR_INVALID_STATE(
2135+
env, "The \"buffer\" argument was resized while reading \"options\"");
2136+
return;
2137+
}
21082138

21092139
db->FinalizeStatements();
21102140

@@ -2242,17 +2272,37 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
22422272
return;
22432273
}
22442274

2275+
if (!js_len->IsInt32()) {
2276+
THROW_ERR_INVALID_ARG_TYPE(
2277+
env->isolate(),
2278+
"The \"options.step.length\" property must be an integer.");
2279+
return;
2280+
}
2281+
22452282
// Subtract 1 because the first argument is the aggregate value.
22462283
argc = js_len.As<Int32>()->Value() - 1;
2247-
if (!inverseFunc.IsEmpty() &&
2248-
!inverseFunc->Get(env->context(), env->length_string())
2249-
.ToLocal(&js_len)) {
2250-
return;
2284+
if (!inverseFunc.IsEmpty()) {
2285+
if (!inverseFunc->Get(env->context(), env->length_string())
2286+
.ToLocal(&js_len)) {
2287+
return;
2288+
}
2289+
2290+
if (!js_len->IsInt32()) {
2291+
THROW_ERR_INVALID_ARG_TYPE(
2292+
env->isolate(),
2293+
"The \"options.inverse.length\" property must be an integer.");
2294+
return;
2295+
}
22512296
}
22522297

22532298
argc = std::max({argc, js_len.As<Int32>()->Value() - 1, 0});
22542299
}
22552300

2301+
// Reading the options bag and the step/inverse "length" properties above can
2302+
// run user JavaScript through a property getter, which may have closed the
2303+
// database since it was checked.
2304+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2305+
22562306
int text_rep = SQLITE_UTF8;
22572307
if (direct_only) {
22582308
text_rep |= SQLITE_DIRECTONLY;
@@ -2281,10 +2331,15 @@ void DatabaseSync::AggregateFunction(const FunctionCallbackInfo<Value>& args) {
22812331
}
22822332

22832333
void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
2334+
DatabaseSync* db;
2335+
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
2336+
Environment* env = Environment::GetCurrent(args);
2337+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2338+
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
2339+
22842340
std::string table;
22852341
std::string db_name = "main";
22862342

2287-
Environment* env = Environment::GetCurrent(args);
22882343
if (args.Length() > 0) {
22892344
if (!args[0]->IsObject()) {
22902345
THROW_ERR_INVALID_ARG_TYPE(env->isolate(),
@@ -2335,10 +2390,9 @@ void DatabaseSync::CreateSession(const FunctionCallbackInfo<Value>& args) {
23352390
}
23362391
}
23372392

2338-
DatabaseSync* db;
2339-
ASSIGN_OR_RETURN_UNWRAP(&db, args.This());
2393+
// Reading the options bag above can run user JavaScript through a property
2394+
// getter, which may have closed the database since it was checked.
23402395
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2341-
THROW_AND_RETURN_IF_IN_AUTHORIZER(env, db);
23422396

23432397
sqlite3_session* pSession;
23442398
int r =
@@ -2465,6 +2519,11 @@ void Backup(const FunctionCallbackInfo<Value>& args) {
24652519
}
24662520
}
24672521

2522+
// Reading the destination path and the options bag above can run user
2523+
// JavaScript through a property getter, which may have closed the database
2524+
// since it was checked.
2525+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2526+
24682527
Local<Promise::Resolver> resolver;
24692528
if (!Promise::Resolver::New(env->context()).ToLocal(&resolver)) {
24702529
return;
@@ -2608,6 +2667,10 @@ void DatabaseSync::ApplyChangeset(const FunctionCallbackInfo<Value>& args) {
26082667
}
26092668
}
26102669

2670+
// Reading the options bag above can run user JavaScript through a property
2671+
// getter, which may have closed the database since it was checked.
2672+
THROW_AND_RETURN_ON_BAD_STATE(env, !db->IsOpen(), "database is not open");
2673+
26112674
// Keep the database alive in case a callback drops all references to it,
26122675
// which could otherwise let it be garbage-collected mid-callback.
26132676
BaseObjectPtr<DatabaseSync> guard(db);

0 commit comments

Comments
 (0)