Skip to content

fix(tipping): validate NWC URI before saving; add HTTP timeouts on LNURL - #63

Open
oth-body wants to merge 1 commit into
masterfrom
fix/tipping-path-hardening
Open

fix(tipping): validate NWC URI before saving; add HTTP timeouts on LNURL#63
oth-body wants to merge 1 commit into
masterfrom
fix/tipping-path-hardening

Conversation

@oth-body

@oth-body oth-body commented Sep 6, 2026

Copy link
Copy Markdown
Owner

Summary

Two defense-in-depth fixes on the Lightning tipping path (hoot -nwc, hoot -tip).

What this fixes

1. NWC URI was saved to disk without validation

Pre-fix: hoot -nwc garbage silently wrote "garbage" to nwc.txt (mode 0600, but the connection secret was the only auth — if the file ever leaked to a filesystem that strips permissions, the connection was wide open). The user got no error and no indication that the URI was malformed.

Post-fix: a new validateNWCURI(uri) helper runs before any disk write. It rejects:

  • empty input
  • wrong URL scheme (must be nostr+walletconnect://)
  • missing wallet pubkey in the URL authority
  • missing ?relay= query parameter
  • missing ?secret= query parameter

The check is intentionally permissive — any future NIP-47 extension field passes through unchanged — but it catches the cases that would have failed later in a less-actionable way.

2. resolveLud16 and fetchLightningInvoice had no HTTP timeout

Pre-fix: both used the default http.Get with no context and no timeout. If the LNURL endpoint hangs (slow wallet, network partition, malicious response that never closes the stream), hoot -tip would freeze forever waiting for the response. This is the kind of failure that turns a one-shot CLI invocation into a manual kill.

Post-fix: explicit http.Client{Timeout: 10*time.Second} for both calls. The LUD-16 spec recommends a 10s timeout for lnurlp responses, and 10s is short enough to not annoy users on slow networks but long enough to not abort legitimate slow responses.

Tests

nwc_validation_test.go (new, 7 table-driven cases):

URI Expected
"" error: must not be empty
https://example.com error: scheme must be nostr+walletconnect
nostr+walletconnect://?relay=wss://r&secret=abc error: missing pubkey
nostr+walletconnect://pubkey?secret=abc error: missing relay
nostr+walletconnect://pubkey?relay=wss://r error: missing secret
nostr+walletconnect://pubkey123?relay=wss://...&secret=hexsecret accepted
valid + extra params (lud16=...) accepted

Also updated hoot_test.go TestNWCParsing — the old version pinned the bug (passed "invalid-uri" and expected "saved successfully"). New version uses a properly-formatted URI and asserts the save path still works end-to-end.

Conflict check

Two defense-in-depth fixes on the tipping path (hoot -tip, hoot -nwc):

1. validateNWCURI runs before saveNWCURI writes to disk.
   Pre-fix: `hoot -nwc garbage` silently wrote "garbage" to nwc.txt
   (mode 0600, but the secret was the only auth — if the file was
   ever copied to a filesystem that strips perms, the connection
   string leaked). Post-fix: structural check rejects empty / wrong-
   scheme / missing-required-fields URIs immediately with an actionable
   error message before any disk write.

   The existing TestNWCParsing pinned the bug ("invalid-uri" was the
   test input; "saved successfully" was the assertion). Updated to
   use a properly-formatted URI and assert save still works.

2. resolveLud16 and fetchLightningInvoice used the global http.Get
   with no timeout. If the LNURL endpoint hangs (slow wallet,
   network partition, malicious response that never closes the
   stream), `hoot -tip` would freeze forever waiting for the
   response. Now: explicit http.Client{Timeout: 10*time.Second} for
   both. LUD-16 spec recommends 10s for lnurlp responses.

Tests: nwc_validation_test.go (7 table-driven cases) covers
empty/wrong-scheme/missing-fields/valid-with-extras/valid-basic.
Replaces the bug-pinning assertion in hoot_test.go with a behavior-
correct one.

No conflict with #61 (silent-failures-hoot) or #62 (publish-error-
surfacing). All three branches touch hoot.go but at disjoint
functions: #61 = defaultRelays/generateProfileID/payInvoiceNWC,
#62 = main() publish loop + publishNote(), this PR = NWC helpers +
HTTP clients in resolveLud16/fetchLightningInvoice.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant