| 7 |
7 |
|
|
| 8 |
8 |
|
---
|
| 9 |
9 |
|
|
|
10 |
+ |
## Fuzz Findings (2026-04-27)
|
|
11 |
+ |
|
|
12 |
+ |
Findings from adversarial code review. Ordered by severity.
|
|
13 |
+ |
|
|
14 |
+ |
### HIGH
|
|
15 |
+ |
|
|
16 |
+ |
- [x] **XSS in bookmark HTML export** (`commands/bookmarks.rs:403`). The `body` field (attacker-controlled RSS content) is interpolated into the HTML template without escaping. `title`/`url`/`author` are escaped but `body` is not. Fix: pass `body` through `html_escape` or sanitize.
|
|
17 |
+ |
- [x] **Path traversal in `download_and_open`** (`commands/items.rs:384-388`). URL filename extracted via `rsplit('/')` is passed to `dir.join()` without sanitizing `..` components. Fix: strip path separators and `..` from derived filename.
|
|
18 |
+ |
- [x] **Arbitrary code exec via `open::that`** (`commands/items.rs:404`). Downloaded files are opened with the system default handler with no extension validation. A `.exe`/`.command`/`.scpt` URL would be downloaded and launched. Fix: allowlist safe extensions or skip auto-open for dangerous ones.
|
|
19 |
+ |
- [x] **Source+unread/starred filter bypass** (`bb-feed/generator/query.rs:29-61`). The `if/else if` chain gives `source` priority over `unread_only`/`starred_only`. When both are set (no search), the unread/starred filter is silently dropped. Fix: add `list_by_busser_unread`/`list_by_busser_starred` queries, or combine the filters in the existing chain.
|
|
20 |
+ |
- [x] **Regex recompilation per-item** (`ordering.rs:197` + `query.rs:103`). `retain` calls `matches()` which calls `compile_regexes()` per item. O(N×M) regex compilations. Fix: pre-compile once before the `retain` loop.
|
|
21 |
+ |
|
|
22 |
+ |
### MEDIUM — sync service
|
|
23 |
+ |
|
|
24 |
+ |
- [x] **No mutex on `perform_sync`** (`sync_service/mod.rs`, `sync_scheduler.rs`, `commands/sync.rs`). Fixed: added `tokio::sync::Mutex` in `AppState`, acquired in both `sync_now` and `sync_scheduler`.
|
|
25 |
+ |
- [x] **Skipped changelog entries still marked as pushed** (`sync_service/upload.rs:77`). Fixed: track individual pushed IDs; only mark those as pushed. Skipped entries remain `pushed=0` and will be retried.
|
|
26 |
+ |
- [x] **`INSERT OR REPLACE` with partial JSON nulls columns** (`sync_service/download.rs:158-199`). Fixed: validate primary key columns exist before upserting; log debug warning for other missing columns so they're visible but don't block the sync.
|
|
27 |
+ |
- [x] **`applying_remote` flag not in transaction** (`sync_service/download.rs:53-58`). Fixed: flag set, data changes, and flag clear are all wrapped in a single SQLite transaction. A crash mid-apply rolls back the flag too.
|
|
28 |
+ |
|
|
29 |
+ |
### MEDIUM — crypto
|
|
30 |
+ |
|
|
31 |
+ |
- [x] **TOCTOU race in `load_or_create_key`** (`crypto.rs:29-51`). Fixed: uses `OpenOptions::create_new(true)` for atomic check-and-create.
|
|
32 |
+ |
- [x] **Keychain migration deletes file before verifying durability** (`crypto.rs:78-82`). Fixed: read-back and compare before deleting file.
|
|
33 |
+ |
- [x] **Key creation failure silently degrades to plaintext** (`state.rs:68-76`). Fixed: encryption key loading is now a hard startup error. App will not start without a working encryption key.
|
|
34 |
+ |
- [x] **Decrypt failure passes raw ciphertext to plugins** (`crypto.rs:199-200`). Fixed: clears field to empty string on decrypt failure instead of passing ciphertext through.
|
|
35 |
+ |
- [x] **No key zeroing on drop** (`crypto.rs:21-25`). Fixed: all key material wrapped in `zeroize::Zeroizing<[u8; 32]>` via `EncryptionKey` type alias. Keys are zeroed from memory when dropped.
|
|
36 |
+ |
|
|
37 |
+ |
### MEDIUM — database
|
|
38 |
+ |
|
|
39 |
+ |
- [x] **Missing transaction in `BookmarksRepository::create`** (`repository.rs:1047-1085`). Bookmark insert + tag inserts are not transactional; partial failure leaves bookmark with incomplete tags.
|
|
40 |
+ |
- [x] **Missing transaction in `BookmarksRepository::set_tags`** (`repository.rs:1180-1198`). Delete-all + inserts not transactional; failure mid-insert loses tags permanently.
|
|
41 |
+ |
- [x] **`update_config` does not update `updated_at`** (`repository.rs:316-323`). Every other mutation method updates `updated_at`; this one doesn't, which could break sync/cache-invalidation.
|
|
42 |
+ |
|
|
43 |
+ |
### MEDIUM — plugin sandbox
|
|
44 |
+ |
|
|
45 |
+ |
- [x] **No `set_max_string_size`/`set_max_array_size` on Rhai engine** (`rhai_plugin/mod.rs:271-274`). Fixed: set `max_string_size` (2 MB), `max_array_size` (5000), `max_map_size` (2000) on both engine creation sites. Complements the existing per-node `validate_dynamic_sizes` on return values.
|
|
46 |
+ |
- [x] **Rhai `import` may not be disabled** (`rhai_plugin/mod.rs:271`). Fixed: set `DummyModuleResolver` on both `load_plugin` and `create_engine` engines.
|
|
47 |
+ |
- [x] **DNS rebinding bypasses URL blocklist** (`rhai_plugin/host_functions.rs:35-86`). Fixed: strip `user@` from host before blocklist check; added `100.x` (CGNAT/Tailscale) block. Note: DNS rebinding itself is inherent to string-level checks; mitigated by trust model (local plugins only).
|
|
48 |
+ |
|
|
49 |
+ |
### MEDIUM — other
|
|
50 |
+ |
|
|
51 |
+ |
- [x] **`count()` ignores most filters** (`bb-feed/generator/query.rs:160-168`). Fixed: added `starred_only` support and combined source+unread/starred via `list_filtered`.
|
|
52 |
+ |
- [x] **`get_all_items()` ignores `feed_tags` filter** (`bb-feed/generator/query.rs:137-156`). Fixed: added feed_tags filtering before `apply()`.
|
|
53 |
+ |
- [x] **No URL scheme validation in `extract_reader_view`** (`commands/query_feeds.rs:192-210`). Fixed: validates http/https before calling reader script.
|
|
54 |
+ |
- [x] **HTML URL cleaner only matches double-quoted attributes** (`url_cleaner.rs:88`). Single-quoted URLs (`href='...'`) bypass tracking parameter removal entirely.
|
|
55 |
+ |
|
|
56 |
+ |
### LOW / NOTE
|
|
57 |
+ |
|
|
58 |
+ |
- [x] **No size limit on OPML import** (`commands/opml.rs:70`). Fixed: capped at 10 MB before parsing.
|
|
59 |
+ |
- [x] **Bookmark duplicate check not transactional** (`commands/bookmarks.rs:143-145`). Kept check-then-insert (no UNIQUE constraint on url column). Acceptable for desktop single-user app. Documented.
|
|
60 |
+ |
- [x] **No bookmark tag validation** (`commands/bookmarks.rs:274-291`). Fixed: added `validate_bookmark_tags` using same `tagtree::validate_with` rules as feed tags. Applied to `create_bookmark`, `create_bookmark_from_item`, and `set_bookmark_tags`.
|
|
61 |
+ |
- [x] **Error leakage**: raw sqlx errors forwarded to frontend (`commands/error.rs:81-84`). Fixed: log full error server-side, send generic message to frontend.
|
|
62 |
+ |
- [x] **`expect()` on `AppState::new()` in startup** (`lib.rs:39`). Fixed: replaced `expect` with `match` that returns `Err` to Tauri setup, logging + printing the error. No more panic on DB/key failure.
|
|
63 |
+ |
- [x] **`serde_json::to_string` fallback replaces config with `{}`** (`orchestrator.rs:389-390`). Fixed: serialization failure now logs an error and skips the update, preserving existing config.
|
|
64 |
+ |
- [x] **`FeedItemId::to_combined`/`from_combined` roundtrip fails when source contains `:`** (`feed_item.rs:24-32`). Fixed: added `debug_assert` that source must not contain `:`. Documented the invariant. Source IDs are simple ASCII identifiers by convention.
|
|
65 |
+ |
|
|
66 |
+ |
---
|
|
67 |
+ |
|
| 10 |
68 |
|
## Phase 7: Plugin OAuth (Post-beta)
|
| 11 |
69 |
|
|
| 12 |
70 |
|
### 7A: OAuth Infrastructure
|