From dafed3330bb6141295b14ee3e9cc1096b7ea880a Mon Sep 17 00:00:00 2001 From: Avi Date: Fri, 21 Aug 2026 13:31:34 -0500 Subject: [PATCH] Refresh checkpoint with IPC allowlist hardening --- CHECKPOINT-encryption.md | 98 ++++++++++++++++++---------------------- 1 file changed, 45 insertions(+), 53 deletions(-) diff --git a/CHECKPOINT-encryption.md b/CHECKPOINT-encryption.md index e843244..8fa618a 100644 --- a/CHECKPOINT-encryption.md +++ b/CHECKPOINT-encryption.md @@ -1,4 +1,4 @@ -# Checkpoint — Contact-aware feed (2026-08-06) +# Checkpoint — Renderer IPC method allowlist (2026-08-21) A stopping point you can return to if this session is closed. Everything below was verified green at the moment this file was written. @@ -6,46 +6,35 @@ verified green at the moment this file was written. ## Where things are - Project: `/home/avi/Projects/0_Nostr` -- Git repo: `master` @ `a1445d1` ("Add contact-aware feed scope"). -- Working tree is **clean** — this session's contact-aware feed changes and its checkpoint - are committed. -- On top of that sits the NIP-46 signer work and the original feed aggregation, all committed. +- Git repo: `master` @ `4bde395` ("Restrict renderer IPC to an explicit method allowlist"). +- Working tree is **clean** apart from this checkpoint update, which is committed right after. +- Underneath sit the contact-aware feed (`a1445d1`), feed aggregation (`37a9d94`) and the + NIP-46 signer work, all committed. -## What was completed: contact-aware feed +## What was completed: security hardening item #1 (IPC allowlist) -The Feed screen (and CLI) can now be scoped to **My contacts**: instead of showing the global -stream, it fetches the active profile's kind 3 contact list from the enabled relays and returns -only recent notes authored by those contacts. This builds on the existing aggregation engine. +First fix from the 2026-08-21 security audit. The Electron main process now enforces an +explicit allowlist of renderer-callable methods, so a compromised web page inside the app +can no longer invent arbitrary backend calls (e.g. unknown/future methods or typos that +might alias onto sensitive ones). -- **Backend** (`src/feed.rs`): - - `contact_feed(settings, limit, owner_hex)` fetches the owner's contact pubkeys - (`contact_pubkeys`) then aggregates only notes from those authors. - - `contact_pubkeys` subscribes to kind 3 (contact list) for the owner, collecting the hex - pubkeys of everyone they follow from `TagStandard::PublicKey` tags, de-duplicated. - - `aggregate_feed` still serves the global feed and now delegates to a private - `aggregate_for(settings, limit, authors)` used by both scopes. When `authors` is set the - NIP-01 filter restricts to those authors **and** `FeedBuilder` drops any note from someone - else (defence in depth for relays that ignore the `authors` filter). - - `FeedBuilder::new(limit, authors)` takes an optional author whitelist. - - `owner_pubkey(npub)` decodes a stored `npub1...` (or hex) to a `PublicKey`; shared by the - IPC server and the CLI. -- **IPC** (`src/ipc.rs`): `FeedGet` now accepts `contacts_only: Option`. With - `contacts_only: true`, it resolves the active profile's npub, decodes it, and calls - `contact_feed`. Without an active profile it returns a `no_active_profile` error. -- **CLI** (`src/main.rs`): `feed [--contacts] [limit]` — `--contacts` filters to the active - profile's contacts; the no-results message is adjusted accordingly. -- **Electron/React frontend**: the Feed screen (`FeedScreen.tsx`) gains a segmented **Everyone / - My contacts** scope toggle. "My contacts" is disabled when no profile is active (and silently - forced back to Everyone). Scope-aware empty states ("No notes from your contacts"), and - `feedGet(limit, contactsOnly)` threads the flag from `api.ts` through `AppProvider`. -- **Styles** (`styles.css`): a `.segmented` pill control for the scope toggle. +- **Electron** (`frontend/electron/main.ts`): + - New `RENDERER_METHODS` set: the three native methods (`pick_image`, `link_preview`, + `upload_image`) plus every Rust-backend method actually used by `frontend/src/lib/api.ts` + (init/get_state, profiles, publish, feed, relays, settings, vault password/unlock, + reveal_secret_key, signer control/approve). + - `isAllowedMethod()` gate at the top of the `backend:request` handler; anything not in + the list returns `{status:'error', code:'unknown_method'}` and is logged to stderr — + it never reaches the backend process. + - No behaviour change for the real UI: `window.backend.request` is only ever called from + `api.ts`, whose full surface is covered by the allowlist (confirmed by grep + tests). +- Remaining audit items (not yet done): token-based upload path validation (#2), CSP + `'unsafe-inline'` removal + window-open deny (#3), legacy-vault permissions (#4), + permission race windows (#5), key zeroization (#6). ## Commits -- `a1445d1` "Add contact-aware feed scope" — the full feature: `src/feed.rs` - (`contact_feed`/`contact_pubkeys`/`aggregate_for` + author whitelist), IPC `FeedGet` - `contacts_only`, CLI `feed --contacts`, the frontend scope toggle + styles + tests, README. - (Parent: `1fd057d` "Refresh checkpoint with feed aggregation".) +- `4bde395` "Restrict renderer IPC to an explicit method allowlist" — `main.ts` only (+49 lines). ## How it was verified (all green) @@ -53,33 +42,36 @@ only recent notes authored by those contacts. This builds on the existing aggreg cargo test # 84 passed cargo clippy --all-targets # clean cargo fmt --check # clean -cargo build --release # builds (rebuilt so the GUI runs the new backend) +cargo build --release # ok npm run typecheck # clean (frontend/) -npm run lint # clean (pre-existing module warning only) +npm run lint # clean (pre-existing module-type warning only) npm run format:check # clean npm test # 78 passed (14 files) -npm run electron:build # compiles the Electron main process +npm run electron:build # compiles the Electron main process with the new gate npm run build # rebuilds the React bundle (dist/) ``` ## How to resume 1. Open the repo: `cd /home/avi/Projects/0_Nostr` -2. State is committed: `git status` should be clean; `git log --oneline -5` shows `a1445d1` - at the top. -3. To try the contact feed: - - GUI: open the **Feed** screen and switch **Everyone → My contacts**. You need an active - profile that follows people; the segmented control filters the feed to their notes. - - CLI: `./target/release/nostr-manager-backend feed --contacts 10` prints up to 10 recent - notes from the active profile's contacts. -4. Re-run verification with the commands above. +2. State is committed: `git status` should be clean; `git log --oneline -3` shows `4bde395` + on top. +3. Launch as usual: `cd frontend && npm start` (backend already built in `target/release/`). +4. To see the gate working: from the DevTools console run + `window.backend.request('not_a_method')` → resolves to a `unknown_method` error envelope; + normal screens keep working unchanged. +5. Re-run verification with the commands above. ## Outstanding / next steps (if you continue) -- The contact feed depends on the kind 3 contact list being present on the enabled relays; a - profile with no contacts (or a newly created one that has never published a contact list) - produces an empty feed, which is expected but could be surfaced more clearly in the GUI. -- A live end-to-end check of the feed (global and contacts) against public relays has not been - run in this session; the deterministic parts are covered by unit + frontend tests. -- The checkpoint/session discipline in AGENTS.md says to commit the feature first, then the - checkpoint update. This was done: `a1445d1` (feature) then this checkpoint commit. \ No newline at end of file +Security-audit follow-ups in priority order: + +1. **Upload path tokens** — `upload_image` currently reads any renderer-supplied file path; + issue random tokens per picked file and accept only those (`main.ts`). +2. **CSP** — drop `'unsafe-inline'` from `script-src` in `frontend/index.html`; add + `setWindowOpenHandler` deny + `shell.openExternal` for external links in `main.ts`. +3. **Legacy vault perms** — repo-root `profiles_vault.json` is group-readable (0664); + chmod 600 / delete after confirming migration; also auto-tighten during migration in + `src/vault.rs`. +4. **Write-race fixes** — create files with mode 0600 at creation time in `vault.rs`. +5. **Zeroize** — wipe decrypted key material via the `zeroize` crate.