If you discover a security vulnerability, please email the maintainer directly (see the repo's commit history for contact) with:
- A clear description of the issue
- Steps to reproduce (if applicable)
- The impact and severity you assess
- Any suggested fixes
We aim to acknowledge reports within 48 hours and provide an initial assessment within one week. Vulnerabilities are not disclosed publicly until a fix is available and users have had reasonable time to upgrade.
| Asset | At-rest protection | In-memory protection | Renderer isolation |
|---|---|---|---|
| BIP39 seed phrase | Argon2id + AES-256-GCM under user passphrase (noncustodial/vault.rs) |
Time-boxed session; zeroized on lock (noncustodial/session.rs) |
Never crosses into the webview |
| Namebase session cookie | AES-256-GCM under OS-keyring-held DEK (noncustodial/cookie_vault.rs) |
Held only during the HTTP request | Redacted from get_settings; write-denied from renderer |
| hsd node RPC api-key | Redacted from get_settings; write-denied from renderer |
Held in NodeRpcClient struct |
Never sent to the webview |
| hsd node RPC api-key (transport) | N/A (not stored encrypted) | guard_transport rejects remote cleartext HTTP when a key is set; HTTPS required per hsd API guidance |
N/A |
Background sync daemon (namehold-syncd) |
N/A — daemon holds no secrets and never has access to key material | Reads hsd RPC via the shared settings (api-key never leaves the Rust process); read-only against hsd | Rust binary — never touches the webview |
Retired. The legacy Namebase platform (sunset.namebase.io) shut down on 1 October 2026 and answers every request with 410, so the wallet no longer asks for a cookie. A cookie stored before then is still protected as described below until you log out, which clears it. The analysis is kept for that case.
Namebase Sunset (the custodial domain registry) offers no API-token or OAuth mechanism. The session cookie is the only bearer credential Namebase accepts programmatically. This is a limitation of Namebase, not a design choice by Namehold.
Mitigations (3 layers):
- Cookie never sent to renderer:
get_settingsredacts the raw cookie and emits only__has_namebase_cookie: "true"(commands/settings.rs:12-16) - Renderer cannot write the cookie or base URL: both keys are in
RENDERER_WRITE_DENYLIST(security.rs:20);update_settingrejects writes before any DB mutation - Per-transaction confirmation in a separate Rust-owned window: before signing
any transaction,
sign_tx_draftrequires explicit user confirmation (commands/tx.rs::sign_tx_draft_inner,prompt_secure) showing action, recipient, amount, fee, txid, and warnings
Mitigation: The cookie is encrypted at rest under an OS-keyring-held DEK (data-encryption key). The DEK is stored in the OS keyring (macOS Keychain / Windows Credential Manager / Linux Secret Service) and cannot be extracted without the user's OS-login credentials.
Blob format: NBC1(4) || nonce(12) || AES-256-GCM(cookie || tag), hex-encoded
in the namebase_cookie_v1 setting. The ciphertext is useless without the DEK.
Residual risk (honest disclosure): An attacker with concurrent code execution as the logged-in user CAN read the DEK from the OS keyring (the OS trusts the logged-in user). Encryption-at-rest defeats offline attackers, not attackers who have already compromised the running user session. This is the standard threat model for OS-keyring-backed secrets (Signal Desktop, Bitwarden, VS Code, etc.).
Mitigations:
- Release builds:
test_base_url_overridereturns empty (commands/namebase.rs:24-27); the base URL is compile-time locked tohttps://sunset.namebase.io - Debug/test builds:
validate_base_url(namebase/client.rs:184) enforces a strict host allowlist + HTTPS requirement; loopback accepted only undercfg(debug_assertions) - Renderer cannot write the setting:
namebase_base_urlis inRENDERER_WRITE_DENYLIST
The hsd API documentation explicitly states: "If you intend to use API via network and setup api-key, make sure to setup ssl too."
Mitigation: guard_transport (noncustodial/rpc.rs:139) enforces this
requirement:
https://is accepted for any host (key retained)http://is accepted only for loopback addresses (127.0.0.1,::1,localhost)- Remote
http://with a non-empty api-key is rejected with a clear error - If
NodeRpcClient::newis called with a remote HTTP URL, it blanks the api-key defensively (rpc.rs:172-184) so the key is never sent cleartext even if the guard is somehow bypassed
This fully complies with the hsd node API's security guidance.
Users who want zero credential exposure on their device can use Namebase's own web UI to initiate transfers/withdrawals to an address generated in Namehold, then use Namehold's chain-monitoring to confirm arrival. This workflow:
- Requires no session cookie to be stored locally
- Trades batch operations and in-app visibility for zero credential exposure
- Is fully supported by Namehold's core functionality (address generation, chain monitoring, name tracking)
The Namebase session cookie is used only by the migration helper
(commands/namebase.rs). It is never needed for the wallet's core
non-custodial operation (holding keys, signing transactions, broadcasting via
hsd RPC, tracking owned names). A user who disconnects Namebase loses zero
wallet functionality.
The background sync daemon (namehold-syncd) is a separate Rust binary that:
- Runs every 60 seconds when "Sync in background" is enabled (Settings → Connections, default ON).
- Reads wallet profiles (UTXOs, name states, transactions) from the local hsd node via RPC.
- Writes sync data to the shared SQLite database (
~/.namehold/portfolio.db). - Writes its process ID to
~/.namehold/syncd.pidfor lifecycle tracking. - Never signs transactions, never broadcasts, never touches key material.
Mitigation: The app detects a dead daemon on startup and respawns it (if
"Sync in background" is ON). A cross-process DB lock table (sync_locks) uses
heartbeats (every 10 seconds) and stale-lock takeover (after 30 seconds) to
detect and recover from crashes.
Behavior (not a vulnerability): When "Sync in background" is ON, hsd is not killed when the app closes. The daemon keeps it alive for background syncing. This is intentional — the next app launch adopts the running hsd. To stop hsd, disable "Sync in background" or manually click Stop hsd in Settings → Connections.
Residual risk (honest disclosure): An attacker with local code execution as
the app user could potentially interact with the orphaned hsd node (e.g., via
RPC) if they know the API key. This is no worse than if the user left hsd
running manually. Mitigations: run hsd on loopback only (127.0.0.1), use a
strong API key, and disable "Sync in background" if you're concerned about
orphaned processes.
Mitigation: The DB lock table ensures only one reader/writer is active at a time. The app's manual Sync and the daemon coordinate via heartbeats and stale takeover.
Guarantee: The daemon never has access to key material, never signs transactions, and never broadcasts. Even if the daemon is compromised, it cannot steal funds or sign malicious transactions. It can only read and write sync data.
The "never broadcasts" half is a runtime rule, not a property of the build: the daemon runs the same run_sync_steps as the app, and the Shakedex purchase refresh in it holds the one path that can send (a purchase's single rebroadcast, already signed by the app). That path is closed for the daemon by Rebroadcast::Never: daemon_sync_makes_no_send_call_where_the_apps_sync_does runs the daemon's own sync_profile against a mock hsd holding a purchase due for its rebroadcast and checks that no sendrawtransaction reaches it, while the app's sync of the same wallet sends one; daemon_never_rebroadcasts_and_leaves_it_to_the_app checks the refresh itself under Rebroadcast::Never. Signing stays impossible in the daemon: it has no key material.
SPV (Simplified Payment Verification) mode runs hsd with --spv instead of
--index-address --index-tx. This means:
- No address indexing — the node doesn't track which coins belong to which address.
- No transaction indexing — the node doesn't store full transaction details.
- Read-only — the wallet cannot send transactions in SPV mode.
- Explorer-dependent — balance and name data come from the configured explorer.
-
Explorer trust: In SPV mode, the wallet trusts the explorer for balance and name data. If the explorer is compromised, it could show incorrect data. Mitigation: configure a trusted explorer and optionally set a fallback URL.
-
No local verification: Unlike full node mode, SPV mode doesn't verify transactions locally against the full chain. The explorer is the source of truth for reads.
-
Read-only guarantee: SPV mode blocks all write operations (sending, name actions) at the write-capability check level. Even if the explorer is compromised, it cannot trick the wallet into signing malicious transactions.
-
hsd still runs locally: The SPV node still runs on your machine and handles block header verification. The explorer is only used for data reads, not for transaction validation.
- Quick setup: When you want to see your balance immediately without waiting for full sync.
- Low-disk environments: When you can't afford ~15GB for a full node.
- Monitoring: When you only need to watch names/auctions without sending.
- Sending transactions: Switch to full node mode to send.
- High-security requirements: Full node mode provides stronger security guarantees.
The Market lists names for sale through Shakedex, read from the LearnHNS Market (https://market.learnhns.com) or from a listing file the user imports. Buying one signs a transaction that spends the seller's lock coin with the seller's presigned signature and pays the seller from the wallet's own coins.
Mitigation: Nothing the market says is trusted. Every listing is checked against the profile's own node before Buy is offered and again when the purchase is built: the lock coin must exist, hold this name and match the listing's lock address, and every price step's signature must verify. Just before broadcast the price is checked again, and the purchase is refused, and its draft discarded, if a cheaper step has become valid since it was reviewed, the step it pays is no longer valid (the median time went back, as in a reorg or on a node behind), or the node cannot say (no median time, no answer): the user reviews and signs it again. The one automatic rebroadcast of a purchase that went missing checks it the same way: at a dropped price the purchase is not sent again, and is given up with nothing paid. SPV and Explorer profiles cannot buy, because they have no node to check against.
Mitigation: The client talks only to the LearnHNS host over HTTPS and follows no redirects. Replies are capped (8 MiB for market pages, 256 KiB for a listing file). The base URL override (learnhns_base_url) is read only in debug builds, accepts only the LearnHNS host or loopback, and the renderer cannot write it (RENDERER_WRITE_DENYLIST).
Mitigation: The seller's lock coin is a foreign input carrying its own witness; the wallet signs only its own inputs. A Ledger signs every input as the wallet's own P2WPKH, so a plan with a foreign input, a custom sequence or a lock time is refused twice: when the draft is signed (commands::tx) and in the Ledger signer itself (providers::ledger::signing).
Guarantee: The background daemon refreshes each purchase's state from the chain but never rebroadcasts one. A purchase that went missing is rebroadcast at most once, and only by the app's own sync, through the same broadcast gates as every other send.
| Concern | Mitigation | Location | Tests |
|---|---|---|---|
| Renderer reads cookie | Redacted; presence marker only | settings.rs:12-16 |
settings_cmd_tests |
| Renderer writes cookie | RENDERER_WRITE_DENYLIST |
security.rs:20 |
settings_cmd_tests |
| Renderer writes base URL | RENDERER_WRITE_DENYLIST |
security.rs:20 |
settings_cmd_tests |
| Base URL redirect (production) | Release build ignores setting | namebase.rs:24-27 |
namebase.rs::tests |
| Cookie on disk (offline attacker) | AES-256-GCM under OS-keyring DEK | cookie_vault.rs |
cookie_vault::tests |
| Signing without confirmation | Rust-owned secure window | tx.rs::sign_tx_draft_inner (prompt_secure) |
tx_lifecycle_tests |
| RPC api-key sent cleartext | guard_transport rejects remote HTTP |
rpc.rs:139-168 |
rpc.rs::tests |
| Audit log leaks secrets | Redacted to *** on write; re-redacted on read |
settings.rs:40-41, 68-69 |
settings_cmd_tests |
| Market redirect or host swap | HTTPS LearnHNS host only, no redirects, override debug-only | market/learnhns.rs |
learnhns_tests |
| Tampered listing or price | Verified on the profile's node; price re-checked before broadcast | noncustodial/shakedex/verify.rs |
shakedex_verify_tests, shakedex_cmd_tests |
| Ledger signs a foreign input | Refused at draft signing and in the signer | commands/tx.rs, providers/ledger/signing.rs |
ledger_plan_guard_tests |
| Daemon rebroadcasts a purchase | SyncCaller::Daemon never rebroadcasts |
commands/sync.rs, shakedex_jobs.rs |
shakedex_purchase_state_tests::{daemon_sync_makes_no_send_call_where_the_apps_sync_does, daemon_never_rebroadcasts_and_leaves_it_to_the_app} |
CI runs an advisory-only audit job (cargo audit + pnpm audit --prod) on
every PR to surface newly-disclosed CVEs in the dependency graph.
A suppressed advisory is one we have reviewed and determined does not apply to this app (or is unfixable because it is frozen inside an upstream dependency's transitive graph). Every suppression MUST be justified here.
Suppressions live in pnpm-workspace.yaml under auditConfig.ignoreGhsas.
| Advisory | Package | Rationale |
|---|---|---|
| GHSA-qwww-vcr4-c8h2 | react-router |
CSRF bypass that the advisory states "only affects your application if you are using the unstable RSC APIs". Namehold is a Tauri single-page app with client-side routing only — it does not use React Server Components, so the vulnerable code path is never reached. Revisit when upgrading to react-router@>=8.3.0. |
Suppressions live in src-tauri/.cargo/audit.toml under [advisories] ignore.
Every crate below is a transitive dependency of Tauri v2 — none are direct
dependencies of this crate.
| Advisory | Package | Kind | Rationale |
|---|---|---|---|
| RUSTSEC-2026-0194, RUSTSEC-2026-0195 | quick-xml 0.39.4 |
vuln (DoS) | Fixed only in >=0.41.0 (semver-major). Pinned to 0.39.x by plist (^0.39.2) and wayland-scanner (^0.39) inside Tauri v2. Not exposed to untrusted XML at runtime: plist parses the app's own macOS Info.plist at bundle time; wayland-scanner parses local Wayland protocol XML at build time. Revisit when Tauri's tree admits quick-xml >=0.41. |
| RUSTSEC-2024-0429 | glib 0.18.5 |
unsound | Unsound VariantStrIter iterators. Pulled in by Tauri v2's wry/tao GTK3 Linux backend; not reachable from app code. |
| RUSTSEC-2024-0370 | proc-macro-error 1.0.4 |
unmaintained | Compile-time-only proc-macro helper via glib-macros / gtk3-macros. No runtime code. |
| RUSTSEC-2024-0411 through -0420 | gtk-rs GTK3 bindings (atk, atk-sys, gdk, gdk-sys, gdkwayland-sys, gdkx11, gdkx11-sys, gtk, gtk-sys, gtk3-macros, all 0.18.2) |
unmaintained | Tauri v2 Linux windowing depends on the GTK3 stack. A fix requires Tauri migrating to GTK4/webkitgtk-6 upstream. |
| RUSTSEC-2025-0075, -0080, -0081, -0098, -0100 | unic-* (unic-char-range, unic-common, unic-char-property, unic-ucd-version, unic-ucd-ident, all 0.9.0) |
unmaintained | Pulled in transitively via urlpattern <- tauri-utils. No direct use. |
anyhow(RUSTSEC-2026-0190) was fixed by bumping to1.0.104rather than suppressed, since it had a semver-compatible patch.
- Vulnerabilities are not disclosed publicly until a fix is available
- Security updates are released as soon as feasible
- Researchers who report responsibly are credited (unless they prefer anonymity)
- README -- feature overview and architecture
- User Manual -- user-facing security guidance
- CHANGELOG -- security fixes and improvements
- hsd API docs -- Handshake node RPC reference
| Daemon crashes mid-sync | Heartbeat every 10s; stale-lock takeover after 30s; app respawns daemon on next startup |
db/sync_lock.rs,commands/daemon_ctl.rs|sync_locktests | | Concurrent writes by app + daemon | Cross-processsync_lockstable; app acquires with priority, daemon preempts stale locks |db/sync_lock.rs,commands/sync.rs|sync_lock,sync_racetests | | Daemon signs (would-be) | Daemon has no access to key material — it only reads hsd and writes sync data |bin/namehold-syncd.rs,daemon/mod.rs| (no key material in the daemon process) | | Daemon broadcasts (would-be) | The one send path inrun_sync_steps(a purchase's rebroadcast) is closed for the daemon at runtime byRebroadcast::Never|commands/sync.rs,shakedex_jobs.rs|shakedex_purchase_state_tests::{daemon_sync_makes_no_send_call_where_the_apps_sync_does, daemon_never_rebroadcasts_and_leaves_it_to_the_app}| | hsd left running after app exit | Intentional when "Sync in background" ON; hsd bound to loopback + api-key required |lib.rs(setup/exit hooks) |settings-background-synctests |