What changed, and why it matters
This change tightens how base64-encoded PSBT (Partially Signed Bitcoin Transaction) data is decoded. Previously, the decoder could silently ignore extra whitespace or unexpected characters in the base64 stream. Now it rejects anything that isn't strict, clean base64. This is a defensive hardening fix: it prevents malformed or cleverly padded base64 inputs from being accepted, which could in theory be used to sneak invalid data past validation or cause inconsistent parsing between different PSBT implementations.
Treat as a low-to-moderate hardening patch. Review whether any legitimate callers rely on whitespace-tolerant base64 PSBT input and update them if needed. Consider adding tests for rejected malformed base64 inputs. No urgent incident response is indicated from the diff alone.
Security signals we found
Strict input validation added for base64 decoding
Rejection of whitespace/newlines inside base64 payload
Use of strict base64 decoder to enforce RFC4648 padding rules
Potential for cross-implementation parsing mismatch reduced
Evidence from the diff
The commit replaces the streaming base64 decoder (base64.NewDecoder) in psbt.NewFromRawBytes with a new helper decodeBase64Strict. The helper reads the entire input, rejects any CR/LF characters, then uses Go’s strict base64 decoder so that ‘=’ is only allowed as final padding and no other non-alphabet characters are tolerated. This makes PSBT base64 parsing conform more closely to RFC4648 and prevents lenient decoding behavior that could accept subtly malformed packets.
Changed components
psbt/psbt.gopsbt.NewFromRawBytesPSBT base64 deserialization pathInspect captured patch +28 / −5
diff --git a/psbt/psbt.go b/psbt/psbt.go
index 8f47f28..d8e39f8 100644
--- a/psbt/psbt.go
+++ b/psbt/psbt.go
@@ -188,12 +188,12 @@ func NewFromUnsignedTx(tx *wire.MsgTx) (*Packet, error) {
// NOTE: To create a Packet from one's own data, rather than reading in a
// serialization from a counterparty, one should use a psbt.New.
func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) {
- // If the PSBT is encoded in bas64, then we'll create a new wrapper
- // reader that'll allow us to incrementally decode the contents of the
- // io.Reader.
if b64 {
- based64EncodedReader := r
- r = base64.NewDecoder(base64.StdEncoding, based64EncodedReader)
+ decoded, err := decodeBase64Strict(r)
+ if err != nil {
+ return nil, err
+ }
+ r = bytes.NewReader(decoded)
}
// The Packet struct does not store the fixed magic bytes, but they
@@ -331,6 +331,29 @@ func NewFromRawBytes(r io.Reader, b64 bool) (*Packet, error) {
return &newPsbt, nil
}
+// decodeBase64Strict decodes an RFC4648 base64 stream without permitting
+// whitespace and with '=' allowed only as final padding.
+func decodeBase64Strict(r io.Reader) ([]byte, error) {
+ encoded, err := io.ReadAll(r)
+ if err != nil {
+ return nil, err
+ }
+
+ // Go's strict base64 decoder still ignores CR/LF. Reject them before
+ // decoding so base64 PSBT parsing matches the RFC4648 alphabet exactly.
+ if bytes.ContainsAny(encoded, "\r\n") {
+ return nil, ErrInvalidPsbtFormat
+ }
+
+ decoded := make([]byte, base64.StdEncoding.DecodedLen(len(encoded)))
+ n, err := base64.StdEncoding.Strict().Decode(decoded, encoded)
+ if err != nil {
+ return nil, ErrInvalidPsbtFormat
+ }
+
+ return decoded[:n], nil
+}
+
// Serialize creates a binary serialization of the referenced Packet struct
// with lexicographical ordering (by key) of the subsections.
func (p *Packet) Serialize(w io.Writer) error {
Why this scored 46/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.