What changed, and why it matters
This commit adds a safety check in Electrum's Lightning message parser to reject invalid cryptographic 'points' (public-key-like values) instead of silently accepting them. Previously, malformed points could pass through parsing and potentially cause problems later. The change is defensive and aligns with official BOLT12 test vectors that expect invalid points to be rejected.
Treat as a hardening fix; include in normal release testing. Review whether other primitive field types (e.g., signatures, hashes) receive equivalent validation and whether downstream consumers of parsed point fields assume validation has occurred.
Security signals we found
Input validation added for cryptographic point fields
Previously skipped negative test now enforced
Malformed point now raises structured MalformedMsg exception
Aligns with failing BOLT12 test vectors containing invalid points
Evidence from the diff
In electrum/lnmsg.py, _read_primitive_field now validates ‘point’ typed fields by instantiating ecc.ECPubkey for each 33-byte chunk. If instantiation fails with InvalidECPointException, a MalformedMsg exception is raised. A previously skipped test in tests/test_lnmsg.py is now enabled and expects MalformedMsg for an invalid ECC point. This hardens TLV parsing against malformed BOLT12 offers but does not by itself demonstrate an exploitable vulnerability.
Changed components
electrum/lnmsg.py:_read_primitive_fieldLightning/BOLT12 message deserializationtests/test_lnmsg.pyInspect captured patch +9 / −3
diff --git a/electrum/lnmsg.py b/electrum/lnmsg.py
index c6cf34d..7faa737 100644
--- a/electrum/lnmsg.py
+++ b/electrum/lnmsg.py
@@ -194,6 +194,13 @@ def _read_primitive_field(
except UnicodeDecodeError as e:
raise MalformedMsg(f'invalid utf-8: {buf.hex()}') from e
+ if field_type == 'point':
+ for point in chunks(buf, type_len):
+ try:
+ ecc.ECPubkey(b=point)
+ except ecc.keys.InvalidECPointException as e:
+ raise MalformedMsg(f"invalid point: {point.hex()}") from e
+
return buf
diff --git a/tests/test_lnmsg.py b/tests/test_lnmsg.py
index b7ca817..d542c44 100644
--- a/tests/test_lnmsg.py
+++ b/tests/test_lnmsg.py
@@ -116,9 +116,8 @@ class TestLNMsg(ElectrumTestCase):
lnser.read_tlv_stream(fd=io.BytesIO(bfh("0329023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001")), tlv_stream_name="n1")
with self.assertRaises(UnexpectedEndOfStream):
lnser.read_tlv_stream(fd=io.BytesIO(bfh("0330023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb000000000000000100000000000001")), tlv_stream_name="n1")
- # check if ECC point is valid?... skip for now.
- #with self.assertRaises(Exception):
- # lnser.read_tlv_stream(fd=io.BytesIO(bfh("0331043da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002")), tlv_stream_name="n1")
+ with self.assertRaises(MalformedMsg): # check if ECC point is valid
+ lnser.read_tlv_stream(fd=io.BytesIO(bfh("0331043da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb00000000000000010000000000000002")), tlv_stream_name="n1")
with self.assertRaises(MsgTrailingGarbage):
lnser.read_tlv_stream(fd=io.BytesIO(bfh("0332023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb0000000000000001000000000000000001")), tlv_stream_name="n1")
with self.assertRaises(UnexpectedEndOfStream):
Why this scored 47/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.