diff options
| author | Adam Soltys <[email protected]> | 2026-06-15 23:35:50 -0700 |
|---|---|---|
| committer | Adam Soltys <[email protected]> | 2026-06-15 23:35:50 -0700 |
| commit | 497277ef7b48c86aa2f44b613e0a35a0c3b0c238 (patch) | |
| tree | 5a05d5f2f1bc739d9bd76f96b5eaeb94f09224fa /routes | |
| parent | 35df18174ab61c6d4a72bf8f1157584adb14bc4f (diff) | |
| download | coinos-server-497277ef7b48c86aa2f44b613e0a35a0c3b0c238.tar.gz coinos-server-497277ef7b48c86aa2f44b613e0a35a0c3b0c238.zip | |
lnurl: SSRF-guard user-supplied URL fetches + validate zap events
The /lnurl/proxy, /decode and /encode handlers fetched an arbitrary
user-supplied URL via got() and returned the body — an unauthenticated
SSRF reaching internal services. Route those fetches through a new
safeGot() that rejects hosts resolving to loopback/private/link-local/
CGNAT/metadata ranges, re-validates on each redirect hop, restricts the
scheme to http(s), and caps timeout + response size. Also validate the
NIP-57 zap request (kind 9734 + signature) before storing it, instead of
trusting the client-supplied event.
These endpoints can't be removed — they back lightning-address/LNURL
parsing and resolution in the wallet UI.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Diffstat (limited to 'routes')
| -rw-r--r-- | routes/lnurl.ts | 14 |
1 files changed, 9 insertions, 5 deletions
diff --git a/routes/lnurl.ts b/routes/lnurl.ts index 2c7c2158..c96a9160 100644 --- a/routes/lnurl.ts +++ b/routes/lnurl.ts @@ -4,7 +4,8 @@ import { err, warn } from "$lib/logging"; import { serverPubkey2 } from "$lib/nostr"; import { SATS, bail, fail, getInvoice, getUser } from "$lib/utils"; import { bech32 } from "bech32"; -import got from "got"; +import { safeGot } from "$lib/safe-fetch"; +import { verifyEvent } from "nostr-tools"; import { v4 } from "uuid"; import { PaymentType } from "$lib/types"; @@ -24,7 +25,7 @@ export default { .replace(/\s/g, "")}`; try { - const r = await got(url).json(); + const r = await safeGot(url); if (r.tag !== "payRequest") fail("not an ln address"); } catch (e) { const m = `failed to lookup lightning address ${address}`; @@ -45,7 +46,7 @@ export default { bech32.fromWords(bech32.decode(text, 20000).words), ).toString(); - const r = await got(url).json(); + const r = await safeGot(url); res.send(r); } catch (e) { bail(res, e.message); @@ -116,7 +117,10 @@ export default { if (nostr) { try { const event = JSON.parse(decodeURIComponent(nostr)); - // TODO: validate the event + // NIP-57: must be a signed kind-9734 zap request. Reject anything + // else so we can't be tricked into storing a forged zap receipt. + if (event.kind !== 9734 || !verifyEvent(event)) + throw new Error("invalid zap request"); await s(`zap:${id}`, event); metadata = nostr; } catch (e) { @@ -154,7 +158,7 @@ export default { const { url } = req.query; if (!url) return bail(res, "url required"); try { - const r = await got(url).json(); + const r = await safeGot(url); res.send(r); } catch (e) { warn("lnurl proxy failed", url, e.message); |
