-
-
Notifications
You must be signed in to change notification settings - Fork 961
Add detailed vulnerability report for lowdb async adapter #617
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,76 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- GITHUB SECURITY ADVISORY SUBMISSION --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Title: Silent Data Loss via Write Coalescing in steno (lowdb dependency) --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Severity: Medium --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Ecosystem: npm --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Package name: lowdb --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Affected versions: <= 7.0.1 --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- Patched versions: (leave blank) --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <!-- CWE: CWE-362 --> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Summary | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| lowdb v7.0.1's async file adapter depends on steno v4.0.2, whose `Writer` silently drops intermediate writes when a write is in progress. All `write()` promises resolve successfully, but only the last queued write is persisted to disk. This is a data integrity violation — callers receive a false durability guarantee, leading to silent data loss under concurrent load. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ## Details | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| When a steno `Writer` is locked (write in progress), subsequent `write()` calls are handled by `#add()` (`steno/lib/index.js`, lines 35-45): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| When a steno `Writer` is locked (write in progress), subsequent `write()` calls are handled by `#add()` (`steno/lib/index.js`, lines 35-45): | |
| When a steno `Writer` is locked (write in progress), subsequent `write()` calls are handled by `#add()` as implemented in steno v4.0.2 ([source](https://github.com/typicode/steno/blob/v4.0.2/lib/index.js)): |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The PoC demonstrates steno's Writer directly, but the report is about lowdb's async adapter behavior. To substantiate impact for lowdb users, consider adding a lowdb-level PoC (e.g., concurrent request-style read/modify/write using JSONFilePreset/Low.update) showing a lost update scenario, rather than only showing that the last raw file write wins.
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Impact section uses absolute language ("Any web application", "majority of writes are lost") which is likely misleading; the failure mode depends on whether the application performs concurrent read-modify-write operations without a mutex/queue. Please qualify the conditions under which data loss occurs and describe the expected behavior (last-write-wins) more precisely.
| Any web application using lowdb's async adapter that handles concurrent requests will silently lose user data: | |
| - API returns **200 OK** to the user, but their data is **never persisted** | |
| - No errors, no warnings — completely silent data loss | |
| - Under typical web server load (multiple concurrent requests), the **majority** of writes are lost | |
| - Affects audit logs, user data, configuration changes — anything stored in lowdb | |
| Web applications that use lowdb's async adapter and perform concurrent write or read-modify-write operations without external synchronization (for example, a mutex or explicit write queue) can silently lose user data due to last-write-wins write coalescing: | |
| - API returns **200 OK** to the user, but their data is **never persisted** | |
| - No errors, no warnings — completely silent data loss | |
| - Under such concurrent access patterns, intermediate writes may be coalesced so that only the last write in a burst is persisted (last-write-wins), and earlier writes are effectively lost | |
| - This can affect audit logs, user data, configuration changes — any state stored through lowdb that relies on each write being durably persisted |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remediation steps 1-2 suggest changing steno's internal behavior, which is outside lowdb's repo and may be a deliberate design choice for steno. Consider adding lowdb-scoped remediation guidance (e.g., serialize db.write/db.update calls per file, provide an optional strict adapter that queues every write, or recommend JSONFileSync where appropriate) and separate any steno-directed recommendations into a distinct section.
| 1. Replace write coalescing with a sequential write queue that persists every write | |
| 2. At minimum, reject or error on coalesced writes instead of silently dropping them | |
| 3. Document the write-coalescing behavior prominently so developers avoid relying on write durability | |
| ### For lowdb users and maintainers | |
| 1. When using lowdb's async file adapter, **serialize `db.write`/`db.update` calls per database file** (for example, via an application-level queue or mutex) so that concurrent writes are not issued against the same underlying `Writer` instance. | |
| 2. Provide or recommend an optional **"strict" lowdb adapter** that internally queues every write and ensures each `write()` call is flushed to disk, or clearly recommend the synchronous `JSONFileSync` adapter for workloads that require strong durability. | |
| 3. **Document the async adapter's concurrency and durability characteristics** in lowdb's README and API docs, including the risk of data loss under concurrent writes, and direct users who need stronger guarantees toward safer adapters or serialization patterns. | |
| ### Upstream steno remediation (outside this repository) | |
| These changes would need to be implemented in `steno` itself and are listed here for the upstream maintainer: | |
| 1. Replace write coalescing with a sequential write queue that persists every write. | |
| 2. At minimum, reject or error on coalesced writes instead of silently dropping them. | |
| 3. Document the write-coalescing behavior prominently so developers avoid relying on write durability. |
Copilot
AI
Mar 7, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file reads like a GitHub Security Advisory draft (including a PoC) rather than a SECURITY.md security policy for the repository. Consider moving this content to a private GHSA draft / separate disclosure document, and using SECURITY.md for reporting instructions and supported versions, to avoid publishing exploit details in the repo before a fix is available.
| <!-- GITHUB SECURITY ADVISORY SUBMISSION --> | |
| <!-- Title: Silent Data Loss via Write Coalescing in steno (lowdb dependency) --> | |
| <!-- Severity: Medium --> | |
| <!-- Ecosystem: npm --> | |
| <!-- Package name: lowdb --> | |
| <!-- Affected versions: <= 7.0.1 --> | |
| <!-- Patched versions: (leave blank) --> | |
| <!-- CWE: CWE-362 --> | |
| ## Summary | |
| lowdb v7.0.1's async file adapter depends on steno v4.0.2, whose `Writer` silently drops intermediate writes when a write is in progress. All `write()` promises resolve successfully, but only the last queued write is persisted to disk. This is a data integrity violation — callers receive a false durability guarantee, leading to silent data loss under concurrent load. | |
| ## Details | |
| When a steno `Writer` is locked (write in progress), subsequent `write()` calls are handled by `#add()` (`steno/lib/index.js`, lines 35-45): | |
| ```javascript | |
| #add(data) { | |
| this.#nextData = data; // OVERWRITES any previously queued data | |
| this.#nextPromise ||= new Promise((resolve, reject) => { | |
| this.#next = [resolve, reject]; | |
| }); | |
| return new Promise((resolve, reject) => { | |
| this.#nextPromise?.then(resolve).catch(reject); | |
| }); | |
| } | |
| ``` | |
| If writes A, B, C are queued while write #1 is in progress: | |
| 1. `#add(A)` → `#nextData = A` | |
| 2. `#add(B)` → `#nextData = B` (A silently dropped) | |
| 3. `#add(C)` → `#nextData = C` (B silently dropped) | |
| 4. Write #1 completes → only C is written | |
| 5. **All three promises resolve** — callers of `write(A)` and `write(B)` believe their data was persisted | |
| ## PoC | |
| ```javascript | |
| import { Writer } from 'steno'; | |
| const writer = new Writer('test.json'); | |
| // Fire 10 concurrent writes | |
| const promises = []; | |
| for (let i = 0; i < 10; i++) { | |
| promises.push(writer.write(JSON.stringify({ id: i }))); | |
| } | |
| await Promise.all(promises); | |
| // All 10 promises resolved ✓ | |
| // Read back from disk: | |
| import fs from 'node:fs'; | |
| const persisted = JSON.parse(fs.readFileSync('test.json', 'utf-8')); | |
| console.log(persisted); // { id: 9 } — only the LAST write survived | |
| // Writes 0-8 (90%) silently lost despite successful promise resolution | |
| ``` | |
| **Tested and confirmed:** 9 out of 10 writes (90%) silently dropped. All 10 `write()` promises resolved successfully, giving callers a false durability guarantee. | |
| ## Impact | |
| Any web application using lowdb's async adapter that handles concurrent requests will silently lose user data: | |
| - API returns **200 OK** to the user, but their data is **never persisted** | |
| - No errors, no warnings — completely silent data loss | |
| - Under typical web server load (multiple concurrent requests), the **majority** of writes are lost | |
| - Affects audit logs, user data, configuration changes — anything stored in lowdb | |
| ## Remediation | |
| 1. Replace write coalescing with a sequential write queue that persists every write | |
| 2. At minimum, reject or error on coalesced writes instead of silently dropping them | |
| 3. Document the write-coalescing behavior prominently so developers avoid relying on write durability | |
| **Note:** This vulnerability originates in the `steno` package (same maintainer). A separate advisory may be appropriate for `steno` itself. | |
| # Security Policy | |
| This project takes security seriously and welcomes responsible disclosure of any vulnerabilities. | |
| ## Supported Versions | |
| We aim to support the most recent major versions of this project. Security fixes are generally provided for: | |
| - The latest released major version | |
| - The previous major version, where feasible | |
| Older versions may no longer receive security updates. If you rely on an unsupported version, consider upgrading to a maintained release. | |
| ## Reporting a Vulnerability | |
| If you discover a security vulnerability, please report it responsibly and do not disclose it publicly until it has been investigated and fixed. | |
| You can report vulnerabilities via: | |
| - GitHub Security Advisories (recommended): use the "Report a vulnerability" feature in the repository’s **Security** tab, which will create a private advisory thread with the maintainers. | |
| - Alternatively, contact the maintainers via the email address listed in the repository (e.g., in `README.md` or project metadata), using the subject line `Security vulnerability report`. | |
| When reporting, please include: | |
| - A description of the issue and its potential impact | |
| - Steps to reproduce (if possible) | |
| - Any relevant configuration or environment details | |
| We will: | |
| - Acknowledge receipt of your report as soon as possible | |
| - Investigate the issue and assess its impact | |
| - Work on a fix and coordinate a responsible disclosure timeline | |
| - Publish a security advisory with details once a fix is available and users have a reasonable opportunity to update | |
| To avoid exposing users to unnecessary risk, please do not include full exploit proofs-of-concept or detailed attack instructions in public issues or pull requests. Use the private reporting channels above instead. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The Summary frames steno write coalescing as a bug that violates durability guarantees, but lowdb currently relies on this behavior (see src/adapters/node/TextFile.test.ts where concurrent writes are asserted to result in the last value). Please align the wording with lowdb's actual contract (e.g., that async adapters are last-write-wins under concurrency) and focus the vulnerability claim on lost updates when callers do concurrent read-modify-write without serialization.