Merge pull request #10999 from MrKalipo/fix/bip32-strpath-index-range
What changed, and why it matters
This commit fixes a bug in how Electrum converts text-based Bitcoin wallet key paths (like "m/44'/0'/0'") into numeric form. Previously, typing an index of 2^31 or higher could silently be treated as a hardened key or could make the hardening marker (the apostrophe) meaningless. The fix now rejects such out-of-range indexes with a clear error. This is a correctness and safety improvement for a function that handles sensitive wallet derivation paths, but the commit itself does not describe a specific exploit or security incident.
Review callers of convert_bip32_strpath_to_intpath to confirm they handle ValueError appropriately and do not fall back to unsafe defaults. Consider whether user-facing path input elsewhere (CLI, GUI, plugins) already validates before reaching this function. No urgent patch action is indicated beyond applying the commit.
Security signals we found
Input validation gap in BIP32 path parsing
Potential silent reinterpretation of unhardened index as hardened
Potential no-op of explicit hardened marker for oversized literal index
Boundary-condition tests added for 2^31 and 2^32
No CVE, advisory, or exploit described in commit materials
Evidence from the diff
The function convert_bip32_strpath_to_intpath in electrum/bip32.py previously computed child_index = abs(x_int) | prime before checking the result against UINT32_MAX. Because BIP32 child indexes are 32-bit values where the most-significant bit (0x80000000) indicates hardening, a literal input >= 2^31 would either silently set the hardened bit (when no apostrophe/h was present) or leave the hardened bit already set so the explicit hardening marker had no effect. The patch moves the range check before the OR with the hardened bit, rejecting abs(x_int) >= BIP32_PRIME (2^31). Tests are added for boundary values 2147483647, 2147483648, and 4294967296.
Changed components
electrum/bip32.pyconvert_bip32_strpath_to_intpathInspect captured patch +20 / −3
### electrum/bip32.py
@@ -353,9 +353,12 @@ def convert_bip32_strpath_to_intpath(n: str) -> List[int]:
x_int = int(x)
except ValueError as e:
raise ValueError(f"failed to parse bip32 path: {(str(e))}") from None
- child_index = abs(x_int) | prime
- if child_index > UINT32_MAX:
- raise ValueError(f"bip32 path child index too large: {child_index} > {UINT32_MAX}")
+ x_int = abs(x_int)
+ if x_int >= BIP32_PRIME:
+ # the top bit is the hardened flag; a literal index >= 2**31 would either
+ # silently become hardened or make the explicit hardened marker a no-op
+ raise ValueError(f"bip32 path child index too large: {x_int} >= {BIP32_PRIME}.")
+ child_index = x_int | prime
path.append(child_index)
return path
### tests/test_bitcoin.py
@@ -814,6 +814,20 @@ def test_convert_bip32_strpath_to_intpath(self):
self.assertEqual([0, 0x80000001, 0x80000001], convert_bip32_strpath_to_intpath("m/0/-1/1'"))
self.assertEqual([], convert_bip32_strpath_to_intpath("m/"))
self.assertEqual([2147483692, 2147488889, 221], convert_bip32_strpath_to_intpath("m/44'/5241h/221"))
+ # largest valid non-hardened / hardened index
+ self.assertEqual([0x7fffffff], convert_bip32_strpath_to_intpath("m/2147483647"))
+ self.assertEqual([0xffffffff], convert_bip32_strpath_to_intpath("m/2147483647'"))
+ self.assertEqual([0xffffffff], convert_bip32_strpath_to_intpath("m/-2147483647"))
+ # literal index >= 2**31 must be rejected: it would either silently become
+ # hardened ("m/2147483648" == "m/0'") or make the hardened marker a no-op
+ with self.assertRaisesRegex(ValueError, r"^bip32 path child index too large: 2147483648 >= 2147483648\.$"):
+ convert_bip32_strpath_to_intpath("m/2147483648")
+ with self.assertRaisesRegex(ValueError, r"^bip32 path child index too large: 2147483648 >= 2147483648\.$"):
+ convert_bip32_strpath_to_intpath("m/2147483648'")
+ with self.assertRaisesRegex(ValueError, r"^bip32 path child index too large: 2147483648 >= 2147483648\.$"):
+ convert_bip32_strpath_to_intpath("m/-2147483648")
+ with self.assertRaisesRegex(ValueError, r"^bip32 path child index too large: 4294967296 >= 2147483648\.$"):
+ convert_bip32_strpath_to_intpath("m/4294967296")
def test_convert_bip32_intpath_to_strpath(self):
self.assertEqual("m/0/1h/1h", convert_bip32_intpath_to_strpath([0, 0x80000001, 0x80000001]))Why this scored 49/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.