Skip to content

fix: validate and restrict LNURL request targets - #3597

Draft
reneaaron wants to merge 3 commits into
masterfrom
fix/lnurl-request-target-validation
Draft

fix: validate and restrict LNURL request targets#3597
reneaaron wants to merge 3 commits into
masterfrom
fix/lnurl-request-target-validation

Conversation

@reneaaron

@reneaaron reneaaron commented Aug 24, 2026

Copy link
Copy Markdown
Member

Describe the changes you have made in this PR

LNURL endpoints supplied by a website (via window.webln.lnurl, or an lnurl found on the page) are requested from the extension's privileged background/prompt context, which holds broad host permissions. This restricts which targets those requests may reach.

  • New src/common/lib/lnurlValidation.ts:
    • assertAllowedLnurlUrl(url) — requires https: (allows http: only for .onion) and rejects loopback, private (RFC1918), CGNAT, link-local, cloud-metadata, multicast and reserved targets. IPv6 literals are expanded before classification, so IPv4-mapped (::ffff:127.0.0.1), IPv4-compatible and NAT64 (64:ff9b::) forms are recognised after the URL parser re-serialises them as hex; fe80::/10, fc00::/7 and fec0::/10 are covered.
    • assertAllowedCallbackUrl(callback, lnurlUrl) — a callback may stay on the origin of the LNURL that produced it; otherwise it must satisfy the same restrictions. Cross-host callbacks (common for lightning addresses) keep working.
    • lnurlGet(url, config, opts) — an axios GET that validates the target and, for website-supplied LNURLs, does not follow redirects.
  • Applies the guard to the detail request (common/lib/lnurl.ts), the lnurl-auth login request (actions/lnurl/auth.ts) and the pay / withdraw / channel callbacks.
  • getDetails takes a userInitiated flag. Send and LNURLRedeem set it, so an LNURL the user pasted or scanned — which may be a self-hosted service on a local network over http — keeps working and may follow redirects. The website-driven paths are unaffected.
  • Failed detail requests report a generic message instead of relaying the upstream response text; the LNURL service's own status: "ERROR" reason is still shown, since the endpoint host has been validated.

Behaviour changes worth noting

  • Website-supplied LNURLs must be https (except .onion) and cannot point at a local/private address. User-pasted LNURLs are unrestricted.
  • Redirects are not followed for website-supplied LNURLs. Per-hop re-validation is not possible in a browser (a manual redirect is opaque, with no readable Location), so they are refused rather than re-checked. Worth a second opinion: if a provider redirects /.well-known/lnurlp/<user> to another host, paying that address from a website would now fail. User-initiated payments still follow redirects.
  • A lightning address that cannot be reached now says so, instead of claiming the address is invalid.

Tests

  • src/common/lib/__tests__/lnurlValidation.test.ts — host classification, plus URL-level assertions that go through the WHATWG parser (the form the validator actually receives), and callback origin handling.
  • The jsdom test environment now provides fetch/Request/Response and the matching abort primitives from one realm, so the axios fetch adapter (and msw) work under test.

yarn lint, yarn tsc:compile and yarn test:unit all pass (76 suites, 209 tests, 2 pre-existing skips).

LNURL endpoints are supplied by the visited website but fetched from a
privileged context with broad host permissions. Restrict those fetches:

- require https (http only for .onion), and reject loopback, private,
  link-local, CGNAT and cloud-metadata hosts, for the LNURL detail
  request, the lnurl-auth login request, and the pay/withdraw/channel
  callbacks
- refuse to follow redirects on these requests so a permitted host
  cannot bounce them to a denied one
- return a fixed message when a detail request fails instead of relaying
  the upstream response text

Adds a shared lnurlValidation helper with unit coverage, and bridges
Node's global fetch into the jsdom test environment so the fetch adapter
(and msw) work under test.
@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown
Contributor

Important

Draft PR not reviewed

Draft PRs are not automatically reviewed by default.

  • Trigger a manual review

To automatically review draft PRs, update your CodeRabbit configuration:

reviews:
  auto_review:
    drafts: true

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

- expand IPv6 literals before classifying them, so IPv4-mapped
  (::ffff:127.0.0.1), IPv4-compatible and NAT64 (64:ff9b::) forms are
  recognised after the URL parser re-serialises them as hex, and add
  site-local fec0::/10
- assert the blocked hosts through the URL parser in the tests, since
  that is the form the validator actually receives
- keep the LNURL service's own error text (LUD-06 status: "ERROR") now
  that the endpoint host is validated, and drop the message that claimed
  a lightning address was invalid when the server was merely unreachable
- take the abort primitives from the same realm as fetch in the test
  environment
The restrictions are there because a website can hand the extension an
endpoint that is then requested from a privileged context. An LNURL the
user pasted or scanned themselves is a target they chose, and may well
be a self-hosted service on a local network over http.

- getDetails takes a userInitiated flag; Send and LNURLRedeem set it, so
  those keep working against a local service and may follow redirects.
  The website-driven paths (webln.lnurl, PublisherLnData) are unchanged.
- callbacks are allowed on the origin of the LNURL that produced them,
  and otherwise have to satisfy the usual restrictions, so a self-hosted
  service's own callback works while a cross-host callback still cannot
  point at a private address.
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