mulit: don't set customData on the lnrpc route level
What changed, and why it matters
This change fixes a display/logic bug in LND's RPC layer where ordinary custom payment data could be incorrectly labeled as 'custom channel data' when returned over the API. It only affects how information is presented to callers, not how funds are moved on the Lightning network. The patch makes the API more accurate but does not appear to be a direct funds-loss vulnerability.
Treat as a regular bug-fix commit. Reviewers should verify that callers of MarshallRoute no longer receive misleading CustomChannelData, and that custom-channel-aware parsers still populate the field when they actually transform data. No urgent security response is indicated by the diff alone.
Security signals we found
Information-disclosure-style API mislabeling: ordinary TLV records exposed under a custom-channel-specific field
RPC-layer only: no change to payment routing, HTLC wire handling, or on-disk payment state
Test expectation changed from equality check to require.Nil for CustomChannelData on non-custom-channel payments
No mention of CVE, security advisory, researcher credit, or exploit in commit message
Evidence from the diff
The commit modifies MarshallRoute in lnrpc/routerrpc/router_backend.go so that the CustomChannelData field on an lnrpc.Route is only populated when an aux-data parser actually transforms the data. Previously, the field could be set even when the parser left the data unchanged, causing non-custom-channel TLV records (such as first-hop custom records or endorsement bits carried in UpdateAddHTLC) to be surfaced as custom channel data. The related tests and comments are updated to reflect that FirstHopCustomRecords/FirstHopWireCustomRecords travel via UpdateAddHTLC and are not custom channel data unless explicitly parsed as such.
Changed components
lnrpc/routerrpc/router_backend.go (MarshallRoute)itest/lnd_forward_interceptor_test.go (forward interceptor restart test)payments/db/payment.go (comment only)routing/route/route.go (comment only)Inspect captured patch +26 / −17
diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go
index e8c1f41..8eec171 100644
--- a/itest/lnd_forward_interceptor_test.go
+++ b/itest/lnd_forward_interceptor_test.go
@@ -1,7 +1,6 @@
package itest
import (
- "bytes"
"fmt"
"reflect"
"strings"
@@ -15,7 +14,6 @@ import (
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes"
- "github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/stretchr/testify/require"
"google.golang.org/grpc/codes"
@@ -477,17 +475,9 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
rt.FirstHopAmountMsat)
}
- cr := lnwire.CustomRecords(p.FirstHopCustomRecords)
- recordData, err := cr.Serialize()
- if err != nil {
- return err
- }
-
- if !bytes.Equal(rt.CustomChannelData, recordData) {
- return fmt.Errorf("expected custom records to "+
- "be equal, got %x expected %x",
- rt.CustomChannelData, recordData)
- }
+ // Make sure the custom channel data is nil because
+ // this is not a custom channel payment.
+ require.Nil(ht, rt.CustomChannelData)
return nil
},
diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go
index 377d0be..d19127f 100644
--- a/lnrpc/routerrpc/router_backend.go
+++ b/lnrpc/routerrpc/router_backend.go
@@ -1,6 +1,7 @@
package routerrpc
import (
+ "bytes"
"context"
"crypto/rand"
"encoding/hex"
@@ -622,10 +623,26 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
// Allow the aux data parser to parse the custom records into
// a human-readable JSON (if available).
if r.ParseCustomChannelData != nil {
+ // Store the original custom data to check if parsing
+ // changed it.
+ originalCustomData := make([]byte, len(customData))
+ copy(originalCustomData, customData)
+
err := r.ParseCustomChannelData(resp)
if err != nil {
return nil, err
}
+
+ // We make sure we only set this field if the parser
+ // changed the data otherwise we might mistakenly
+ // show other tlv custom wire data as custom channel
+ // data.
+ if bytes.Equal(
+ originalCustomData, resp.CustomChannelData,
+ ) {
+
+ resp.CustomChannelData = nil
+ }
}
}
diff --git a/payments/db/payment.go b/payments/db/payment.go
index f6d9983..9b7fe66 100644
--- a/payments/db/payment.go
+++ b/payments/db/payment.go
@@ -90,7 +90,8 @@ type PaymentCreationInfo struct {
// FirstHopCustomRecords are the TLV records that are to be sent to the
// first hop of this payment. These records will be transmitted via the
- // wire message only and therefore do not affect the onion payload size.
+ // wire message (UpdateAddHTLC) only and therefore do not affect the
+ // onion payload size.
FirstHopCustomRecords lnwire.CustomRecords
}
diff --git a/routing/route/route.go b/routing/route/route.go
index b3e91a6..349a2bc 100644
--- a/routing/route/route.go
+++ b/routing/route/route.go
@@ -524,9 +524,10 @@ type Route struct {
]
// FirstHopWireCustomRecords is a set of custom records that should be
- // included in the wire message sent to the first hop. This is only set
- // on custom channels and is used to include additional information
- // about the actual value of the payment.
+ // included in the wire message sent to the first hop. This is for
+ // example used in custom channels. Besides custom channels we use it
+ // also for the endorsement bit. This data will be sent to the first
+ // hop in the UpdateAddHTLC message.
//
// NOTE: Since these records already represent TLV records, and we
// enforce them to be in the custom range (e.g. >= 65536), we don't use
Why this scored 35/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.