bugfix: block SIGHASH_SINGLE by default
What changed, and why it matters
This update changes the COLDCARD hardware wallet so that it refuses to sign Bitcoin transactions using the SIGHASH_SINGLE signature mode unless the user has explicitly enabled a 'Warn' setting. SIGHASH_SINGLE lets a signer lock in only one output while leaving other outputs changeable later. That could allow someone to alter where remaining funds go after the COLDCARD has already signed. The change adds SIGHASH_SINGLE to the existing safety check that already blocked SIGHASH_NONE, and it includes a test to confirm the block works.
Treat this as a security hardening fix. Review whether any user workflows depend on signing with SIGHASH_SINGLE by default, and ensure documentation clearly explains how to enable Sighash Checks Warn mode if that workflow is still needed. No immediate incident response is indicated, but the change should be included in the next release notes.
Security signals we found
Behavior change that blocks a signature type previously allowed by default
New FatalPSBTIssue raised for SIGHASH_SINGLE and SIGHASH_SINGLE|ANYONECANPAY
Existing SIGHASH_NONE blocking logic extended to cover SIGHASH_SINGLE
Changelog explicitly describes the security rationale: later outputs remain modifiable after signing
New regression test added for the disallowed sighash cases
Evidence from the diff
In shared/psbt.py, consider_dangerous_sighash() now tracks SIGHASH_SINGLE and SIGHASH_SINGLE|ANYONECANPAY in addition to the previously tracked unusual sighashes. When the ‘sighshchk’ setting is disabled (the default), the code raises FatalPSBTIssue(‘Sighash SINGLE is not allowed as some outputs could be changed.’). When sighshchk is enabled, it falls through to the warning path. A new test in testing/test_sign.py verifies that both SINGLE and SINGLE|ANYONECANPAY are rejected by default with the expected error message. The changelog credits @instagibbs for reporting the issue.
Changed components
shared/psbt.pytesting/test_sign.pyreleases/Next-ChangeLog.mdInspect captured patch +19 / −0
### releases/Next-ChangeLog.md
@@ -11,6 +11,8 @@ This lists the new changes that have not yet been published in a normal release.
- Bugfix: BIP-322 message signing now rejects non-ASCII and other unsupported
message text before approval. Thanks to @KirillCherikov for reporting.
- Bugfix: Prevent duplicate WIF Store entries after restarting
+- Change: Block `SIGHASH_SINGLE` and `SIGHASH_SINGLE|ANYONECANPAY` by default because they can leave later transaction outputs modifiable after signing. They remain available when Sighash Checks is set to Warn.
+ Thanks to [@instagibbs](https://github.com/instagibbs) for reporting this issue.
# Mk Specific Changes
### shared/psbt.py
@@ -1725,6 +1725,7 @@ def consider_dangerous_sighash(self):
sh_unusual = False
none_sh = False
+ single_sh = False
for input in self.inputs:
# only if it is our input - one that will be eventually sign
@@ -1742,6 +1743,8 @@ def consider_dangerous_sighash(self):
if input.sighash in (SIGHASH_NONE, SIGHASH_NONE|SIGHASH_ANYONECANPAY):
none_sh = True
+ elif input.sighash in (SIGHASH_SINGLE, SIGHASH_SINGLE|SIGHASH_ANYONECANPAY):
+ single_sh = True
if sh_unusual and not settings.get("sighshchk"):
if self.consolidation_tx:
@@ -1752,6 +1755,9 @@ def consider_dangerous_sighash(self):
# sighash NONE or NONE|ANYONECANPAY is proposed: block
raise FatalPSBTIssue("Sighash NONE is not allowed as funds could be going anywhere.")
+ if single_sh:
+ raise FatalPSBTIssue("Sighash SINGLE is not allowed as some outputs could be changed.")
+
if none_sh:
self.warnings.append(
("Danger", "Destination address can be changed after signing (sighash NONE).")
### testing/test_sign.py
@@ -2333,6 +2333,17 @@ def test_sighash_disallowed_NONE(sighash, _test_single_sig_sighash):
consolidation=False, sh_checks=True)
+@pytest.mark.parametrize("sighash", ["SINGLE", "SINGLE|ANYONECANPAY"])
+def test_sighash_disallowed_SINGLE(sighash, fake_txn, start_sign, end_sign,
+ settings_remove):
+ settings_remove("sighshchk")
+ psbt = fake_txn(1, 2, segwit_in=True, change_outputs=[1], sighashes=[sighash])
+ start_sign(psbt, False, stxn_flags=STXN_VISUALIZE)
+ with pytest.raises(Exception) as e:
+ end_sign(accept=None, expect_txn=False)
+ assert "Sighash SINGLE is not allowed as some outputs could be changed" in e.value.args[0]
+
+
@pytest.mark.bitcoind
def test_sighash_nonexistent(_test_single_sig_sighash):
# invalid sighash valueWhy this scored 64/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.