Skip to content

v2.5.0 — External Security Audit

Choose a tag to compare

@alexfrmn alexfrmn released this 20 Aug 15:23
· 21 commits to main since this release

First Murmur release built substantially from external contributions. A security audit by @fedoseevstanislav and wake/delivery analysis by @alexanderyswork account for most of what changed here.

Security

Signed and bound delivery acknowledgements (#100)

ACK correlation previously trusted attacker-controlled JSON carrying only {msgId, status}. Anyone able to publish to an ACK subject could mark an arbitrary pending outbox row acked or failed — suppressing delivery or forcing retries — without authenticating as the purported consumer.

ACKs are now a versioned SignedAckV1: an Ed25519 signature over the message digest, conversation, ACK sender, intended recipient, status, timestamp and nonce. Wrong-message, wrong-conversation, wrong-recipient, wrong-peer, stale, future-dated, invalid-signature and replayed ACKs are rejected, and ACK/NACK state changes apply atomically only from the sent state. Invalid attempts are metered by bounded reason as metadata-only security events — raw ACK and message bodies are never logged.

Migration is deliberately two-stage: upgraded daemons emit signed ACKs that legacy peers still parse, and strict rejection stays opt-in behind ackSecurity.requireSigned / MURMUR_REQUIRE_SIGNED_ACKS=1 until every peer is upgraded.

⚠️ Read this before assuming you are protected. A configuration with no ackSecurity block runs emitSigned=true with requireSigned=false — unsigned ACKs are still accepted. Upgrade every peer, then set ackSecurity.requireSigned: true (or MURMUR_REQUIRE_SIGNED_ACKS=1). Until that flip the issue is mitigated, not closed. Four further gaps in the compatible path — non-durable replay nonces, a fast-ACK race, an unsigned raw-NACK sink in the A2A bridge, and the unguarded WebSocket ACK path — are tracked in #109.

Hardened local state handling (#101)

Agent configs hold long-term signing and encryption private keys plus broker credentials, and rewrites could return them to 0664; SQLite files holding decrypted history were commonly 0644. The daemon now sets umask 0077, creates state directories 0700, atomically writes secret JSON as 0600, rejects symlinked, non-regular and wrong-owner config paths, reads configs with O_NOFOLLOW and re-checks the opened descriptor, and forces database/WAL/shm files to 0600.

Dashboard rendering and ingress (#102)

Untrusted fields render through DOM textContent only — no innerHTML, inline scripts or inline handlers — behind a strict CSP and the full protective header set. HTTP and WebSocket access require Basic authentication from a private server-local token file, and live messages are verified for envelope schema, signature, subject/recipient binding, traffic direction and known-peer identity before reaching the UI. Fails closed without a valid token file.

Fixed

  • Codex wake seeded threads are usable (#97) — thread/start no longer discards thread.path, and new threads carry peer.cwd instead of cwd: null, which produced wrong workspace roots, missing project instructions and wrong permissions.
  • Per-peer baseInstructions no longer dropped (#98) — reaching the injector's peer.resume === false opt-out from real configuration for the first time.
  • Broken quickstartgit clone .../murmur.git && cd mur-mur-v2 failed on the very first command every new user ran. The repository was renamed on 2026-08-03; README, publish tooling and the deploy script kept the old slug.

Documentation

  • CHANGELOG.md has a [2.4.0] section for the first time — that release shipped on 2026-06-23 pointing at "See CHANGELOG.md for details" while the section never existed.
  • SECURITY.md now matches how the project actually behaves: private reporting is still preferred, but a missed acknowledgement target explicitly frees the reporter to disclose publicly. Written after we left a real audit unanswered for nine days.

Known gaps, tracked

The delivery-semantics findings from #96 are filed rather than silently carried: #105 (failed wakes advance the cursor; relay not idempotent — one package, not two), #106 (empty finalText logs as relayed), #107 (sequential drain causes head-of-line blocking), #108 (threadId is process-memory only and scoped per peer).

Held deliberately: #103 (TLS + per-peer NATS auth) needs a coordinated broker and peer credential cutover, and #104 is the wire-breaking ACK variant under evaluation against #100.

npm

@murmurv2/core 0.4.0, @murmurv2/broker-nats 0.3.0, @murmurv2/mcp-server 0.2.0.


213/213 tests pass; CI green on 4a069d8.