What changed, and why it matters
This commit adds support for a new style of LNURL web link (so-called LUD-17 format, e.g. lnurlp://example.com or lnurlw://example.com) in Electrum. It also registers Electrum as the handler for these new link types on Android, Windows, macOS, and Linux. The change is a feature addition, not a fix for a known vulnerability. There is no evidence in the commit or supplied references that this is a security patch or that it addresses any reported security issue.
Treat as a routine feature commit. Reviewers may want to confirm that downstream LNURL fetching code handles arbitrary https URLs safely and that the new URI handlers cannot be abused for local command execution via the Windows registry command string, though the diff itself does not introduce such a vulnerability.
Security signals we found
New URI scheme registration increases Electrum's attack surface for malicious links (lnurlp/lnurlw)
Parser validates scheme against an explicit allow-list and requires hostname/path
No input sanitization beyond urllib.parse.urlsplit is visible in the diff
No security relevance claimed by commit message or vendor references
Evidence from the diff
The patch extends Electrum’s payment identifier parsing to recognize lnurlp:// and lnurlw:// URI schemes, converting them to ordinary https (or http for .onion) URLs before processing as LNURL. It updates OS-level URI registrations (Android intent filter, Windows NSIS registry entries, macOS CFBundleURLSchemes, Linux .desktop MimeType) and the QML Android intent handler. A new helper maybe_extract_url_from_lud_17_uri validates the scheme against SUPPORTED_LNURL_SCHEMES and requires a non-empty hostname and path. Tests cover an unsupported scheme (lnurlc) and a valid lnurlw URI.
Changed components
electrum/payment_identifier.pyelectrum/lnurl.pyelectrum/gui/qml/qeapp.pycontrib/android/bitcoin_intent.xmlcontrib/build-wine/electrum.nsicontrib/osx/pyinstaller.specelectrum.desktoptests/test_payment_identifier.pyInspect captured patch +53 / −5
diff --git a/contrib/android/bitcoin_intent.xml b/contrib/android/bitcoin_intent.xml
index 87553b2..3592e45 100644
--- a/contrib/android/bitcoin_intent.xml
+++ b/contrib/android/bitcoin_intent.xml
@@ -5,4 +5,6 @@
<category android:name="android.intent.category.BROWSABLE" />
<data android:scheme="bitcoin" />
<data android:scheme="lightning" />
+ <data android:scheme="lnurlw" />
+ <data android:scheme="lnurlp" />
</intent-filter>
diff --git a/contrib/build-wine/electrum.nsi b/contrib/build-wine/electrum.nsi
index 6aa4ce2..3eeab10 100644
--- a/contrib/build-wine/electrum.nsi
+++ b/contrib/build-wine/electrum.nsi
@@ -189,7 +189,7 @@ Section
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME} Testnet.lnk" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" "--testnet" "$INSTDIR\electrum-${PRODUCT_VERSION}.exe" 0
- ;Links bitcoin: and lightning: URIs to Electrum
+ ;Links bitcoin:, lightning: and lnurl LUD-17 URIs to Electrum
WriteRegStr HKCU "Software\Classes\bitcoin" "" "URL:bitcoin Protocol"
WriteRegStr HKCU "Software\Classes\bitcoin" "URL Protocol" ""
WriteRegStr HKCU "Software\Classes\bitcoin" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
@@ -198,6 +198,14 @@ Section
WriteRegStr HKCU "Software\Classes\lightning" "URL Protocol" ""
WriteRegStr HKCU "Software\Classes\lightning" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
WriteRegStr HKCU "Software\Classes\lightning\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
+ WriteRegStr HKCU "Software\Classes\lnurlp" "" "URL:lnurlp Protocol"
+ WriteRegStr HKCU "Software\Classes\lnurlp" "URL Protocol" ""
+ WriteRegStr HKCU "Software\Classes\lnurlp" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
+ WriteRegStr HKCU "Software\Classes\lnurlp\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
+ WriteRegStr HKCU "Software\Classes\lnurlw" "" "URL:lnurlw Protocol"
+ WriteRegStr HKCU "Software\Classes\lnurlw" "URL Protocol" ""
+ WriteRegStr HKCU "Software\Classes\lnurlw" "DefaultIcon" "$\"$INSTDIR\electrum.ico, 0$\""
+ WriteRegStr HKCU "Software\Classes\lnurlw\shell\open\command" "" "$\"$INSTDIR\electrum-${PRODUCT_VERSION}.exe$\" $\"%1$\""
;Adds an uninstaller possibility to Windows Uninstall or change a program section
WriteRegStr HKCU "${PRODUCT_UNINST_KEY}" "DisplayName" "$(^Name)"
diff --git a/contrib/osx/pyinstaller.spec b/contrib/osx/pyinstaller.spec
index 5cd612e..6adb551 100644
--- a/contrib/osx/pyinstaller.spec
+++ b/contrib/osx/pyinstaller.spec
@@ -135,7 +135,7 @@ app = BUNDLE(
'CFBundleURLTypes':
[{
'CFBundleURLName': 'bitcoin',
- 'CFBundleURLSchemes': ['bitcoin', 'lightning', ],
+ 'CFBundleURLSchemes': ['bitcoin', 'lightning', 'lnurlp', 'lnurlw', ],
}],
'LSMinimumSystemVersion': '11',
'NSCameraUsageDescription': 'Electrum would like to access the camera to scan for QR codes',
diff --git a/electrum.desktop b/electrum.desktop
index 7434829..8d8d9ed 100644
--- a/electrum.desktop
+++ b/electrum.desktop
@@ -15,7 +15,7 @@ StartupNotify=true
StartupWMClass=electrum
Terminal=false
Type=Application
-MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning;
+MimeType=x-scheme-handler/bitcoin;x-scheme-handler/lightning;x-scheme-handler/lnurlp;x-scheme-handler/lnurlw;
Actions=Testnet;
Keywords=crypto;currency;BTC
diff --git a/electrum/gui/qml/qeapp.py b/electrum/gui/qml/qeapp.py
index e397156..8bf7cc7 100644
--- a/electrum/gui/qml/qeapp.py
+++ b/electrum/gui/qml/qeapp.py
@@ -23,6 +23,7 @@ from electrum.network import Network
from electrum.plugin import run_hook
from electrum.gui.common_qt.util import get_font_id
from electrum.util import profiler
+from electrum.lnurl import SUPPORTED_LNURL_SCHEMES
from .qeconfig import QEConfig
from .qedaemon import QEDaemon
@@ -235,7 +236,9 @@ class QEAppController(BaseCrashReporter, QObject):
data = str(intent.getDataString())
self.logger.debug(f'received intent: {repr(data)}')
scheme = str(intent.getScheme()).lower()
- if scheme == BITCOIN_BIP21_URI_SCHEME or scheme == LIGHTNING_URI_SCHEME:
+ if scheme == BITCOIN_BIP21_URI_SCHEME \
+ or scheme == LIGHTNING_URI_SCHEME \
+ or scheme in SUPPORTED_LNURL_SCHEMES:
self.uriReceived.emit(data)
def startup_finished(self):
diff --git a/electrum/lnurl.py b/electrum/lnurl.py
index a95bf8b..e08f254 100644
--- a/electrum/lnurl.py
+++ b/electrum/lnurl.py
@@ -21,6 +21,9 @@ from electrum.i18n import _
_logger = get_logger(__name__)
+SUPPORTED_LNURL_SCHEMES = ('lnurlp', 'lnurlw')
+
+
class LNURLError(Exception): pass
class UntrustedLNURLError(LNURLError):
diff --git a/electrum/payment_identifier.py b/electrum/payment_identifier.py
index 5883aad..4fac9ad 100644
--- a/electrum/payment_identifier.py
+++ b/electrum/payment_identifier.py
@@ -16,7 +16,7 @@ from .util import get_asyncio_loop, log_exceptions
from .transaction import PartialTxOutput
from .lnurl import (decode_lnurl, request_lnurl, callback_lnurl, LNURLError,
lightning_address_to_url, try_resolve_lnurlpay, LNURL6Data,
- LNURL3Data, LNURLData)
+ LNURL3Data, LNURLData, SUPPORTED_LNURL_SCHEMES)
from .bitcoin import opcodes, construct_script
from .lnaddr import LnInvoiceException
from .lnutil import IncompatibleOrInsaneFeatures
@@ -45,6 +45,22 @@ def remove_uri_prefix(data: str, *, prefix: str) -> str:
return data
+def maybe_extract_url_from_lud_17_uri(data: str) -> Optional[str]:
+ """https://github.com/lnurl/luds/blob/luds/17.md"""
+ data = data.strip()
+ try:
+ parsed = urllib.parse.urlsplit(data)
+ except ValueError:
+ return None
+ if parsed.scheme not in SUPPORTED_LNURL_SCHEMES:
+ return None
+ if not (host := parsed.hostname) or not parsed.path:
+ return None
+ is_onion = host.endswith('.onion')
+ url_scheme = 'http' if is_onion else 'https'
+ return urllib.parse.urlunsplit(parsed._replace(scheme=url_scheme))
+
+
RE_ALIAS = r'(.*?)\s*\<([0-9A-Za-z]{1,})\>'
RE_EMAIL = r'\b[A-Za-z0-9._%+-]+@([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b'
RE_DOMAIN = r'\b([A-Za-z0-9-]+\.)+[A-Z|a-z]{2,7}\b'
@@ -98,6 +114,7 @@ class PaymentIdentifier(Logger):
* openalias
* bip21 URI
* lightning-URI (containing bolt11 or lnurl)
+ * lnurl-URI (lud17 lnurlw/lnurlp URI)
* bolt11 invoice
* lnurl
* lightning address
@@ -228,6 +245,10 @@ class PaymentIdentifier(Logger):
self.logger.debug(f'Exception cause {e.args!r}')
return
self.set_state(PaymentIdentifierState.AVAILABLE)
+ elif lnurl_url := maybe_extract_url_from_lud_17_uri(text):
+ self._type = PaymentIdentifierType.LNURL
+ self.lnurl = lnurl_url
+ self.set_state(PaymentIdentifierState.NEED_RESOLVE)
elif text.lower().startswith(BITCOIN_BIP21_URI_SCHEME + ':'):
try:
out = parse_bip21_URI(text)
diff --git a/tests/test_payment_identifier.py b/tests/test_payment_identifier.py
index 046f5ba..f19fb6b 100644
--- a/tests/test_payment_identifier.py
+++ b/tests/test_payment_identifier.py
@@ -184,6 +184,17 @@ class TestPaymentIdentifier(ElectrumTestCase):
self.assertEqual(PaymentIdentifierType.LNURL, pi.type)
self.assertTrue(pi.need_resolve())
+ # test with lud17 prefix
+ unsupported_lud_17_lnurl_c = f"lnurlc://service.io/?q=3fc3645b439ce8e7"
+ pi = PaymentIdentifier(None, unsupported_lud_17_lnurl_c)
+ self.assertFalse(pi.is_valid())
+
+ valid_lud_17_lnurl_w = f"lnurlw://service.io/?q=3fc3645b439ce8e7"
+ pi = PaymentIdentifier(None, valid_lud_17_lnurl_w)
+ self.assertTrue(pi.is_valid())
+ self.assertEqual(PaymentIdentifierType.LNURL, pi.type)
+ self.assertTrue(pi.need_resolve())
+
@patch('electrum.payment_identifier.request_lnurl')
def test_lnurl_pay_resolve(self, mock_request_lnurl):
"""Test LNURL-pay (LNURL6) with mocked resolve"""
Why this scored 23/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.