qeconfig: fix btc amount regex, add msat regex property, add regex tests
What changed, and why it matters
This commit tightens a Bitcoin amount input validation regex used in Electrum's QML (mobile/desktop) GUI. The old regex lacked start (^) and end ($) anchors, meaning it could match invalid strings that merely contained a valid-looking number somewhere inside. The fix anchors the pattern and makes the decimal fraction optional. A new variant for milli-satoshi precision is also added, along with unit tests. The practical security impact is limited because the regex is only one layer of validation and likely used for UI feedback rather than final payment authorization.
Treat as a routine defensive fix. Review all QML callers of btcAmountRegex/btcAmountRegexMsat to confirm they do not rely solely on the regex for validation, and ensure downstream amount parsing still rejects malformed or out-of-range values. No urgent security response is indicated by the diff alone.
Security signals we found
Input validation regex lacked anchors (^/$), permitting partial-string matches
Regex change is in GUI code (QML) and affects amount entry fields
New unit tests explicitly assert rejection of over-long decimal inputs
No mention of CVE, security advisory, or bug bounty in commit metadata
Evidence from the diff
In electrum/gui/qml/qeconfig.py, btcAmountRegex previously built an expression like ‘[0-9]{0,N}.[0-9]{0,M}’ without anchors. Such a pattern can match substrings, so inputs such as ‘abc1.23def’ or ‘1.000000000’ (too many decimals) might be accepted depending on how the caller uses the regex. The patch anchors the expression with ^ and $, makes the fractional part optional via (.[0-9]{0,M})?, and introduces btcAmountRegexMsat/_btcAmountRegex(extra_precision=3) to allow three extra decimal places for milli-satoshi amounts. Tests are added covering BTC, mBTC, and sat base-unit settings.
Changed components
electrum/gui/qml/qeconfig.pyQEConfig.btcAmountRegex propertyQEConfig.btcAmountRegexMsat propertyQML GUI amount input fields that consume these regexesInspect captured patch +83 / −3
diff --git a/electrum/gui/qml/qeconfig.py b/electrum/gui/qml/qeconfig.py
index 979565e..349f873 100644
--- a/electrum/gui/qml/qeconfig.py
+++ b/electrum/gui/qml/qeconfig.py
@@ -94,14 +94,22 @@ class QEConfig(AuthMixin, QObject):
@pyqtProperty('QRegularExpression', notify=baseUnitChanged)
def btcAmountRegex(self):
+ return self._btcAmountRegex()
+
+ @pyqtProperty('QRegularExpression', notify=baseUnitChanged)
+ def btcAmountRegexMsat(self):
+ return self._btcAmountRegex(3)
+
+ def _btcAmountRegex(self, extra_precision: int = 0):
decimal_point = base_unit_name_to_decimal_point(self.config.get_base_unit())
max_digits_before_dp = (
len(str(TOTAL_COIN_SUPPLY_LIMIT_IN_BTC))
+ (base_unit_name_to_decimal_point("BTC") - decimal_point))
- exp = '[0-9]{0,%d}' % max_digits_before_dp
+ exp = '^[0-9]{0,%d}' % max_digits_before_dp
+ decimal_point += extra_precision
if decimal_point > 0:
- exp += '\\.'
- exp += '[0-9]{0,%d}' % decimal_point
+ exp += '(\\.[0-9]{0,%d})?' % decimal_point
+ exp += '$'
return QRegularExpression(exp)
thousandsSeparatorChanged = pyqtSignal()
diff --git a/tests/test_qml_qeconfig.py b/tests/test_qml_qeconfig.py
index d76c543..f58956b 100644
--- a/tests/test_qml_qeconfig.py
+++ b/tests/test_qml_qeconfig.py
@@ -1,7 +1,11 @@
+from typing import TYPE_CHECKING
from electrum import SimpleConfig
from electrum.gui.qml.qeconfig import QEConfig
from tests.qt_util import QETestCase, qt_test
+if TYPE_CHECKING:
+ from PyQt6.QtCore import QRegularExpression
+
class TestConfig(QETestCase):
@classmethod
@@ -64,3 +68,71 @@ class TestConfig(QETestCase):
self.assertFalse(qa.isEmpty)
self.assertEqual(qa.satsInt, 1)
self.assertEqual(qa.msatsInt, 1001)
+
+ @qt_test
+ def test_btc_amount_regexes(self):
+ self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 8
+
+ a: 'QRegularExpression' = self.q.btcAmountRegex
+ b: 'QRegularExpression' = self.q.btcAmountRegexMsat
+
+ self.assertTrue(a.isValid())
+ self.assertTrue(b.isValid())
+
+ self.assertTrue(a.match('1').hasMatch())
+ self.assertTrue(a.match('1.').hasMatch())
+ self.assertTrue(a.match('1.00000000').hasMatch())
+ self.assertFalse(a.match('1.000000000').hasMatch())
+ self.assertTrue(a.match('21000000').hasMatch())
+ self.assertFalse(a.match('121000000').hasMatch())
+
+ self.assertTrue(b.match('1').hasMatch())
+ self.assertTrue(b.match('1.').hasMatch())
+ self.assertTrue(b.match('1.00000000').hasMatch())
+ self.assertTrue(b.match('1.00000000000').hasMatch())
+ self.assertFalse(b.match('1.000000000000').hasMatch())
+ self.assertTrue(b.match('21000000').hasMatch())
+ self.assertFalse(b.match('121000000').hasMatch())
+
+ self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 5
+
+ a: 'QRegularExpression' = self.q.btcAmountRegex
+ b: 'QRegularExpression' = self.q.btcAmountRegexMsat
+
+ self.assertTrue(a.isValid())
+ self.assertTrue(b.isValid())
+
+ self.assertTrue(a.match('1').hasMatch())
+ self.assertTrue(a.match('1.').hasMatch())
+ self.assertTrue(a.match('1.00000').hasMatch())
+ self.assertFalse(a.match('1.000000').hasMatch())
+ self.assertTrue(a.match('21000000000').hasMatch())
+ self.assertFalse(a.match('121000000000').hasMatch())
+
+ self.assertTrue(b.match('1').hasMatch())
+ self.assertTrue(b.match('1.').hasMatch())
+ self.assertTrue(b.match('1.0000000').hasMatch())
+ self.assertTrue(b.match('1.00000000').hasMatch())
+ self.assertFalse(b.match('1.000000000000').hasMatch())
+ self.assertTrue(b.match('21000000000').hasMatch())
+ self.assertFalse(b.match('121000000000').hasMatch())
+
+ self.q.config.BTC_AMOUNTS_DECIMAL_POINT = 0
+
+ a: 'QRegularExpression' = self.q.btcAmountRegex
+ b: 'QRegularExpression' = self.q.btcAmountRegexMsat
+
+ self.assertTrue(a.isValid())
+ self.assertTrue(b.isValid())
+
+ self.assertTrue(a.match('1').hasMatch())
+ self.assertFalse(a.match('1.').hasMatch())
+ self.assertTrue(a.match('2100000000000000').hasMatch())
+ self.assertFalse(a.match('12100000000000000').hasMatch())
+
+ self.assertTrue(b.match('1').hasMatch())
+ self.assertTrue(b.match('1.').hasMatch())
+ self.assertTrue(b.match('1.000').hasMatch())
+ self.assertFalse(b.match('1.0000').hasMatch())
+ self.assertTrue(b.match('2100000000000000').hasMatch())
+ self.assertFalse(b.match('12100000000000000').hasMatch())
Why this scored 25/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.