From 0191bdd80d8ae08e29bd207dd7acda57a4aa3da3 Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Fri, 25 Sep 2026 15:56:15 +0700 Subject: [PATCH 1/2] fix: reject underscores in BIP32 path indexes Python int() accepts 1_0 as 10, so parse_path("m/1_0") derived index 10. Path components now have to be digits, with the existing harden suffix. --- hwilib/key.py | 15 ++++++++++----- test/test_bip32.py | 6 ++++++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/hwilib/key.py b/hwilib/key.py index 506fb1aad..087a51715 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -335,11 +335,16 @@ def _parse_path(nstr: str, allow_multipath: bool) -> List[List[int]]: def str_to_harden(x: str) -> int: if x.startswith("-"): - return H_(abs(int(x))) - elif x.endswith(("h", "'")): - return H_(int(x[:-1])) - else: - return int(x) + body = x[1:] + if not body.isdigit(): + raise ValueError(f"Invalid BIP32 path component: {x}") + return H_(abs(int(body))) + harden = x.endswith(("h", "'")) + body = x[:-1] if harden else x + if not body.isdigit(): + raise ValueError(f"Invalid BIP32 path component: {x}") + index = int(body) + return H_(index) if harden else index def parse_index(x: str, seen_multipath: bool) -> Tuple[List[int], bool]: if x.startswith("<"): diff --git a/test/test_bip32.py b/test/test_bip32.py index 5a88d8243..0682a7f99 100755 --- a/test/test_bip32.py +++ b/test/test_bip32.py @@ -122,6 +122,12 @@ def test_deriv_path(self): xprv_der = par_xprv.derive_pub_path(path) self.assertEqual(xprv_der.to_string(), child_xpub) + def test_parse_path_rejects_underscore(self): + with self.assertRaises(ValueError): + parse_path("m/1_0") + self.assertEqual(parse_path("m/10"), [10]) + self.assertEqual(parse_path("m/1h"), [0x80000001]) + if __name__ == "__main__": unittest.main() From cee9680e66055463ecf425e1ce6baacdf8c55fed Mon Sep 17 00:00:00 2001 From: Sasha Mitchell Date: Fri, 9 Oct 2026 15:51:49 +0700 Subject: [PATCH 2/2] fix: reject non-ASCII digits and out-of-range BIP32 indexes isdigit() accepts Arabic and fullwidth digits, and an index at 2^31 collides with a hardened zero. --- hwilib/key.py | 11 ++++++++--- test/test_bip32.py | 8 ++++++++ 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/hwilib/key.py b/hwilib/key.py index 087a51715..6e5930c4c 100644 --- a/hwilib/key.py +++ b/hwilib/key.py @@ -336,14 +336,19 @@ def _parse_path(nstr: str, allow_multipath: bool) -> List[List[int]]: def str_to_harden(x: str) -> int: if x.startswith("-"): body = x[1:] - if not body.isdigit(): + if not (body.isascii() and body.isdigit()): raise ValueError(f"Invalid BIP32 path component: {x}") - return H_(abs(int(body))) + index = int(body) + if index >= HARDENED_FLAG: + raise ValueError(f"Invalid BIP32 path component: {x}") + return H_(abs(index)) harden = x.endswith(("h", "'")) body = x[:-1] if harden else x - if not body.isdigit(): + if not (body.isascii() and body.isdigit()): raise ValueError(f"Invalid BIP32 path component: {x}") index = int(body) + if index >= HARDENED_FLAG: + raise ValueError(f"Invalid BIP32 path component: {x}") return H_(index) if harden else index def parse_index(x: str, seen_multipath: bool) -> Tuple[List[int], bool]: diff --git a/test/test_bip32.py b/test/test_bip32.py index 0682a7f99..1e1c31655 100755 --- a/test/test_bip32.py +++ b/test/test_bip32.py @@ -128,6 +128,14 @@ def test_parse_path_rejects_underscore(self): self.assertEqual(parse_path("m/10"), [10]) self.assertEqual(parse_path("m/1h"), [0x80000001]) + def test_parse_path_rejects_non_ascii_digits_and_out_of_range(self): + for path in ("m/١", "m/1h", "m/2147483648", "m/2147483648h", "m/4294967296"): + with self.subTest(path=path): + with self.assertRaises(ValueError): + parse_path(path) + self.assertEqual(parse_path("m/2147483647"), [2147483647]) + self.assertEqual(parse_path("m/2147483647h"), [2147483647 | 0x80000000]) + if __name__ == "__main__": unittest.main()