Skip to content
Closed
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
87 changes: 87 additions & 0 deletions SECURITY.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<!-- GITHUB SECURITY ADVISORY SUBMISSION -->
<!-- Title: Path Traversal via Unsanitized Filename in lowdb Adapter Constructor -->
<!-- Severity: High -->
<!-- Ecosystem: npm -->
<!-- Package name: lowdb -->
<!-- Affected versions: <= 7.0.1 -->
<!-- Patched versions: (leave blank) -->
<!-- CWE: CWE-22 -->

## Summary

lowdb v7.0.1's file-based adapter constructors (`TextFile`, `JSONFile`, `DataFile`) accept a `filename` parameter with zero path validation or sanitization. If any user input influences the database filename, an attacker can read or overwrite arbitrary files on the filesystem via directory traversal or absolute path injection.

## Details

The `TextFile` adapter constructor (`lib/adapters/node/TextFile.js`, lines 8-10) stores the filename verbatim:
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The advisory references lib/adapters/node/TextFile.js (and specific line numbers), but this repository doesn't contain that path—only the TypeScript source exists under src/adapters/node/TextFile.ts. Consider updating the file references (and removing line numbers, or recalculating them against the actual files) so readers can verify the claim in this repo.

Copilot uses AI. Check for mistakes.

```javascript
constructor(filename) {
this.#filename = filename; // Stored verbatim — no validation
this.#writer = new Writer(filename);
}
Comment on lines +18 to +22
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TextFile adapter constructor stores the filename parameter verbatim and passes it directly to Writer without any validation or restriction on the path. If any untrusted input can influence this filename, an attacker can perform directory traversal or absolute path injection to read from or write to arbitrary filesystem locations (including config files, crontabs, or SSH keys). To mitigate this, validate and normalize filename against an allowed base directory (rejecting .. and absolute paths) before use, and avoid allowing callers to specify arbitrary paths.

Copilot uses AI. Check for mistakes.
```

There is no:
- Path normalization or canonicalization
- Rejection of `..` traversal sequences
- Base-directory restriction
- Rejection of absolute paths

The `JSONFilePreset` convenience function (`lib/presets/node.js`, line 4) passes the filename directly through:
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same issue as above: lib/presets/node.js isn't present in this repository (the source is src/presets/node.ts). Update the referenced path/line numbers so the documentation matches the code layout here.

Suggested change
The `JSONFilePreset` convenience function (`lib/presets/node.js`, line 4) passes the filename directly through:
The `JSONFilePreset` convenience function (`src/presets/node.ts`) passes the filename directly through:

Copilot uses AI. Check for mistakes.

```javascript
export async function JSONFilePreset(filename, defaultData) {
const adapter = new JSONFile(filename); // No validation
```

Additionally, steno's `Writer` creates a `.tmp` file in the same directory as the target (`steno/lib/index.js`, lines 6-8), so traversal attacks also create attacker-controlled temp files in arbitrary directories.
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The statement about steno's Writer creating a .tmp file isn't verifiable from this repo (steno source isn't included), and lowdb itself already creates a .${basename}.tmp file in TextFileSync (src/adapters/node/TextFile.ts). Consider either citing lowdb's own temp-file behavior (sync adapter) or linking to the exact steno version/source to avoid an inaccurate claim.

Copilot uses AI. Check for mistakes.

## PoC

```javascript
import { JSONFilePreset } from 'lowdb/node';

// Scenario: Multi-tenant app creates per-user databases
// const db = await JSONFilePreset(`databases/${username}.json`, { notes: [] });

// Attack 1 — Absolute path injection:
const db = await JSONFilePreset('/tmp/lowdb_absolute_injection.json', { pwned: true });
await db.write();
// Result: File created at /tmp/lowdb_absolute_injection.json ✓

// Attack 2 — Relative traversal:
// username = "../../../etc/cron.d/backdoor"
// Creates/overwrites /etc/cron.d/backdoor.json

// Attack 3 — Read arbitrary files:
// username = "../../../etc/passwd"
// lowdb tries to JSON.parse /etc/passwd → error may leak file contents
```
Comment on lines +57 to +60
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PoC suggests that a JSON.parse error on a non-JSON file "may leak file contents". In lowdb's current implementation (DataFile.read()JSON.parse), the thrown SyntaxError message doesn't include the full file contents by default; any disclosure would depend on how the host app logs/returns errors. Please reword to avoid overstating the direct impact (e.g., parsing error/DoS unless the app exposes the error or loaded JSON).

Copilot uses AI. Check for mistakes.

**Tested and confirmed:** Successfully wrote to `/tmp/lowdb_absolute_injection.json` via absolute path injection. Source analysis confirms zero path validation in `TextFile`, `DataFile`, and `JSONFile` constructors.

## Impact

Applications that derive lowdb filenames from user input (per-user databases, tenant-specific files, config selection) are vulnerable to:

- **Arbitrary file write** — overwrite config files, crontabs, authorized_keys
- **Arbitrary file read** — error messages from `JSON.parse` failures can leak file contents
- **Temp file creation** in arbitrary directories via steno's `.tmp` file side-effect
Comment on lines +69 to +70
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This "Arbitrary file read" bullet appears to rely on JSON.parse failure error messages leaking file contents, which lowdb does not do directly. It would be more accurate to describe this as "arbitrary file access attempt" / "potential read if the targeted file is valid JSON and the application exposes db contents", or "error-triggered info leak depending on application error handling".

Copilot uses AI. Check for mistakes.

## Remediation

Add path validation in the adapter constructor:

```javascript
import path from 'node:path';

constructor(filename) {
const str = filename.toString();
if (str.includes('..') || path.isAbsolute(str)) {
throw new Error('lowdb: path traversal detected');
}
this.#filename = path.resolve(filename);
Comment on lines +80 to +84
Copy link

Copilot AI Mar 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The remediation snippet converts filename to str but then calls path.resolve(filename). In Node, path.resolve expects a string; passing a non-string PathLike (e.g., Buffer/URL) can throw. Use the already-derived str for all path.* operations, and consider a more robust traversal mitigation than str.includes('..') (e.g., normalize and enforce that the resolved path stays within an allowed base directory).

Suggested change
const str = filename.toString();
if (str.includes('..') || path.isAbsolute(str)) {
throw new Error('lowdb: path traversal detected');
}
this.#filename = path.resolve(filename);
const baseDir = path.resolve(process.cwd());
const str = filename.toString();
const resolvedPath = path.resolve(baseDir, str);
// Ensure the resolved path stays within the allowed base directory
if (!resolvedPath.startsWith(baseDir + path.sep)) {
throw new Error('lowdb: path traversal detected');
}
this.#filename = resolvedPath;

Copilot uses AI. Check for mistakes.
this.#writer = new Writer(this.#filename);
}
```
Loading