Security fixes before deploy; restore CNB exchange rates #3

Merged
jk merged 2 commits from security-fixes into main 2026-10-13 10:25:40 +00:00
Member

Findings from a pre-deploy security review.

  • Nonce reuse. useServerSync encrypted every mutation in a batch with one XChaCha20 nonce. noble/ciphers 2 throws on that, so full syncs and multi-mutation batches (recurring transactions) never reached the server. Old lofik on noble/ciphers 0.5.3 reused the nonce silently, so batched messages on the old sync server are weakly encrypted. Now one nonce per mutation.
  • SQL values. Values were interpolated into SQL unescaped, so a title like Bob's coffee broke the query. Values are now quoted with '' escaping.
  • Headers. x-frame-options: DENY and x-content-type-options: nosniff.
  • CNB rates. corsproxy.io now rejects keyless requests (403 keyless_legacy_url), so CNB rates had stopped loading. A new /cnb?date=dd.mm.yyyy route fetches the fixed CNB URL server-side, so the third-party proxy is gone.
  • Sync storage. Anyone can mint a key and store messages, so the SQLite file is capped at 1 GB (max_page_count). A failed insert is logged and not acked, so the client keeps its pending updates.
  • Full sync size. maxHttpBufferSize raised from 1 MB (about 2,000 transactions) to 10 MB.
  • Non-root. The container runs as bun, with /app/data owned by it. In Dokploy, use a volume mount, not a bind mount.

Checked: astro check clean, build OK. Production server with two browser profiles: apostrophe titles save, full sync wrote 3 rows with 3 distinct nonces and reached the second device. /cnb returns CNB rates from curl and from the page; a malformed date gets 400. Not tested: docker build (no Docker here), and the 1 GB cap being hit.

🤖 Generated with Claude Code

Findings from a pre-deploy security review. - **Nonce reuse.** `useServerSync` encrypted every mutation in a batch with one XChaCha20 nonce. noble/ciphers 2 throws on that, so full syncs and multi-mutation batches (recurring transactions) never reached the server. Old lofik on noble/ciphers 0.5.3 reused the nonce silently, so batched messages on the old sync server are weakly encrypted. Now one nonce per mutation. - **SQL values.** Values were interpolated into SQL unescaped, so a title like `Bob's coffee` broke the query. Values are now quoted with `''` escaping. - **Headers.** `x-frame-options: DENY` and `x-content-type-options: nosniff`. - **CNB rates.** corsproxy.io now rejects keyless requests (403 `keyless_legacy_url`), so CNB rates had stopped loading. A new `/cnb?date=dd.mm.yyyy` route fetches the fixed CNB URL server-side, so the third-party proxy is gone. - **Sync storage.** Anyone can mint a key and store messages, so the SQLite file is capped at 1 GB (`max_page_count`). A failed insert is logged and not acked, so the client keeps its pending updates. - **Full sync size.** `maxHttpBufferSize` raised from 1 MB (about 2,000 transactions) to 10 MB. - **Non-root.** The container runs as `bun`, with `/app/data` owned by it. In Dokploy, use a volume mount, not a bind mount. Checked: `astro check` clean, build OK. Production server with two browser profiles: apostrophe titles save, full sync wrote 3 rows with 3 distinct nonces and reached the second device. `/cnb` returns CNB rates from curl and from the page; a malformed date gets 400. Not tested: `docker build` (no Docker here), and the 1 GB cap being hit. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Fix nonce reuse in sync, escape SQL values, add frame and nosniff headers
All checks were successful
check / check (pull_request) Successful in 40s
9ff005cc9e
Every mutation in a batch was encrypted with the same nonce. noble/ciphers 2
throws on that, so full syncs and recurring batches never reached the server.
The old 0.5 release reused the nonce silently.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
reviewer left a comment

Recommendation: nits

This PR fixes a nonce-reuse bug in the sync encryption (now one nonce+cipher instance per mutation instead of per batch), adds SQL string escaping via a shared q() helper, and adds x-frame-options/x-content-type-options headers to the production server. I verified the per-message nonce fix lines up with the server/schema, which already stores and expects one nonce per message, and that q() is applied consistently everywhere a user-controlled value was previously interpolated raw into SQL.

**Recommendation:** nits This PR fixes a nonce-reuse bug in the sync encryption (now one nonce+cipher instance per mutation instead of per batch), adds SQL string escaping via a shared `q()` helper, and adds `x-frame-options`/`x-content-type-options` headers to the production server. I verified the per-message nonce fix lines up with the server/schema, which already stores and expects one nonce per message, and that `q()` is applied consistently everywhere a user-controlled value was previously interpolated raw into SQL. <!-- reviewer:sha=9ff005cc9e9fe2a49f0702cec58cd73290004fe0 -->
@ -76,1 +72,4 @@
for (const mutation of mutations) {
const nonce = randomBytes(24);
const chacha = xchacha20poly1305(sha256(privKey), nonce);
Member

nit: sha256(privKey) is now recomputed on every loop iteration even though privKey doesn't change per mutation; only the nonce needs to be fresh. Minor, avoidable work in a hot path for large batches.

**nit:** sha256(privKey) is now recomputed on every loop iteration even though privKey doesn't change per mutation; only the nonce needs to be fresh. Minor, avoidable work in a hot path for large batches.
Member

Fixed in 1ff95a7989.

Fixed in 1ff95a7989a8a6995fac29e9561714a213319588. <!-- reviewer:fixed=7668 -->
Proxy CNB rates ourselves, cap sync storage, run as non-root
All checks were successful
check / check (pull_request) Successful in 39s
1ff95a7989
corsproxy.io now rejects keyless requests, so CNB rates had stopped loading.
The sync database is capped at 1 GB, full sync may send up to 10 MB, and the
container runs as bun.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Member

Re the nit on useLofikMutation.ts (key hashing in the loop): fixed in 1ff95a7. The key is hashed once per batch and only the nonce is per mutation.

Re the nit on `useLofikMutation.ts` (key hashing in the loop): fixed in 1ff95a7. The key is hashed once per batch and only the nonce is per mutation.
agent changed title from Fix nonce reuse in sync, escape SQL values, add frame and nosniff headers to Security fixes before deploy; restore CNB exchange rates 2026-10-13 10:23:40 +00:00
reviewer left a comment

Recommendation: looks-good

This PR self-hosts a CNB exchange-rate proxy (replacing corsproxy.io), caps the sync SQLite DB at 1 GB with a max_page_count pragma plus try/catch around the insert transaction, raises socket.io's max message size for full syncs, and switches the Docker container to run as the non-root bun user. It also fixes the previously-flagged nit of recomputing sha256(privKey) on every mutation in a batch.

**Recommendation:** looks-good This PR self-hosts a CNB exchange-rate proxy (replacing corsproxy.io), caps the sync SQLite DB at 1 GB with a max_page_count pragma plus try/catch around the insert transaction, raises socket.io's max message size for full syncs, and switches the Docker container to run as the non-root `bun` user. It also fixes the previously-flagged nit of recomputing sha256(privKey) on every mutation in a batch. <!-- reviewer:sha=1ff95a7989a8a6995fac29e9561714a213319588 -->
jk merged commit 35dd9cc36d into main 2026-10-13 10:25:40 +00:00
jk deleted branch security-fixes 2026-10-13 10:25:40 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
2 participants
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
unbeholden/lofextra!3
No description provided.