Skip to content

Commit 8f94929

Browse files
authored
Merge pull request #9993 from MPins/issue-9904-9915
Validate UTF-8 description and empty route hints when parsing BOLT-11 invoices
2 parents 6c39b9a + 2815d49 commit 8f94929

File tree

4 files changed

+64
-15
lines changed

4 files changed

+64
-15
lines changed

docs/release-notes/release-notes-0.20.0.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,10 @@
2929
- Fixed [shutdown deadlock](https://github.com/lightningnetwork/lnd/pull/10042)
3030
when we fail starting up LND before we startup the chanbackup sub-server.
3131

32+
- Fixed BOLT-11 invoice parsing behavior: [now errors](
33+
https://github.com/lightningnetwork/lnd/pull/9993) are returned when receiving
34+
empty route hints or a non-UTF-8-encoded description.
35+
3236
- [Fixed](https://github.com/lightningnetwork/lnd/pull/10027) an issue where
3337
known TLV fields were incorrectly encoded into the `ExtraData` field of
3438
messages in the dynamic commitment set.

zpay32/decode.go

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import (
77
"fmt"
88
"strings"
99
"time"
10+
"unicode/utf8"
1011

1112
"github.com/btcsuite/btcd/btcec/v2"
1213
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
@@ -18,6 +19,21 @@ import (
1819
"github.com/lightningnetwork/lnd/lnwire"
1920
)
2021

22+
var (
23+
// ErrInvalidUTF8Description is returned if the invoice description is
24+
// not valid UTF-8.
25+
ErrInvalidUTF8Description = errors.New("description is not valid UTF-8")
26+
27+
// ErrLengthNotMultipleOfHopHintLength is returned if the length of the
28+
// route hint data is not a multiple of the hop hint length.
29+
ErrLengthNotMultipleOfHopHint = errors.New("length is not a multiple " +
30+
"of hop hint length")
31+
32+
// ErrEmptyRouteHint is returned if the route hint field contains no hop
33+
// data.
34+
ErrEmptyRouteHint = errors.New("route hint field contains no hop data")
35+
)
36+
2137
// DecodeOption is a type that can be used to supply functional options to the
2238
// Decode function.
2339
type DecodeOption func(*decodeOptions)
@@ -446,6 +462,10 @@ func parseDescription(data []byte) (*string, error) {
446462
return nil, err
447463
}
448464

465+
if !utf8.Valid(base256Data) {
466+
return nil, ErrInvalidUTF8Description
467+
}
468+
449469
description := string(base256Data)
450470

451471
return &description, nil
@@ -565,8 +585,12 @@ func parseRouteHint(data []byte) ([]HopHint, error) {
565585

566586
// Check that base256Data is a multiple of hopHintLen.
567587
if len(base256Data)%hopHintLen != 0 {
568-
return nil, fmt.Errorf("expected length multiple of %d bytes, "+
569-
"got %d", hopHintLen, len(base256Data))
588+
return nil, ErrLengthNotMultipleOfHopHint
589+
}
590+
591+
// Check for empty route hint
592+
if len(base256Data) == 0 {
593+
return nil, ErrEmptyRouteHint
570594
}
571595

572596
var routeHint []HopHint

zpay32/invoice_internal_test.go

Lines changed: 33 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -369,14 +369,25 @@ func TestParse32Bytes(t *testing.T) {
369369
func TestParseDescription(t *testing.T) {
370370
t.Parallel()
371371

372+
testNonUTF8StrData, _ := bech32.ConvertBits(
373+
[]byte(testNonUTF8Str), 8, 5, true,
374+
)
375+
372376
testCupOfCoffeeData, _ := bech32.ConvertBits([]byte(testCupOfCoffee), 8, 5, true)
373377
testPleaseConsiderData, _ := bech32.ConvertBits([]byte(testPleaseConsider), 8, 5, true)
374378

375379
tests := []struct {
376-
data []byte
377-
valid bool
378-
result *string
380+
data []byte
381+
valid bool
382+
result *string
383+
expectedErr error
379384
}{
385+
{
386+
data: testNonUTF8StrData,
387+
valid: false,
388+
expectedErr: ErrInvalidUTF8Description,
389+
result: nil,
390+
},
380391
{
381392
data: []byte{},
382393
valid: true,
@@ -400,6 +411,7 @@ func TestParseDescription(t *testing.T) {
400411
t.Errorf("description decoding test %d failed: %v", i, err)
401412
return
402413
}
414+
require.ErrorIs(t, err, test.expectedErr)
403415
if test.valid && !reflect.DeepEqual(description, test.result) {
404416
t.Fatalf("test %d failed decoding description: "+
405417
"expected \"%s\", got \"%s\"",
@@ -685,27 +697,34 @@ func TestParseRouteHint(t *testing.T) {
685697
testDoubleHopData, _ = bech32.ConvertBits(testDoubleHopData, 8, 5, true)
686698

687699
tests := []struct {
688-
data []byte
689-
valid bool
690-
result []HopHint
700+
data []byte
701+
valid bool
702+
result []HopHint
703+
expectedErr error
691704
}{
692705
{
693-
data: []byte{0x0, 0x0, 0x0, 0x0},
694-
valid: false, // data too short, not multiple of 51 bytes
706+
data: []byte{0x0, 0x0, 0x0, 0x0},
707+
// data too short, not multiple of 51 bytes
708+
valid: false,
709+
expectedErr: ErrLengthNotMultipleOfHopHint,
695710
},
696711
{
697-
data: []byte{},
698-
valid: true,
699-
result: []HopHint{},
712+
data: []byte{},
713+
valid: false,
714+
result: []HopHint{},
715+
expectedErr: ErrEmptyRouteHint,
700716
},
701717
{
702718
data: testSingleHopData,
703719
valid: true,
704720
result: testSingleHop,
705721
},
706722
{
707-
data: append(testSingleHopData, 0x0),
708-
valid: false, // data too long, not multiple of 51 bytes
723+
data: append(testSingleHopData,
724+
[]byte{0x0, 0x0}...),
725+
// data too long, not multiple of 51 bytes
726+
valid: false,
727+
expectedErr: ErrLengthNotMultipleOfHopHint,
709728
},
710729
{
711730
data: testDoubleHopData,
@@ -720,6 +739,7 @@ func TestParseRouteHint(t *testing.T) {
720739
t.Errorf("routing info decoding test %d failed: %v", i, err)
721740
return
722741
}
742+
require.ErrorIs(t, err, test.expectedErr)
723743
if test.valid {
724744
if err := compareRouteHints(test.result, routeHint); err != nil {
725745
t.Fatalf("test %d failed decoding routing info: %v", i, err)

zpay32/invoice_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ var (
5050
0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11,
5151
}
5252

53+
testNonUTF8Str = "1 cup coffee\xff\xfe\xfd"
5354
testEmptyString = ""
5455
testCupOfCoffee = "1 cup coffee"
5556
testCoffeeBeans = "coffee beans"

0 commit comments

Comments
 (0)