rpcperms: allow multiple read-only middleware
What changed, and why it matters
This change loosens a registration rule in LND's RPC middleware system so that multiple 'read-only' middlewares can be registered at the same time. Previously, the code treated all middlewares the same and rejected any second registration that shared a caveat name, even when both were read-only. The patch also adds a guard so that if a read-only middleware somehow tries to replace a message, its replacement is ignored and logged instead of being applied.
Review as a normal bug fix. Verify that read-only middlewares cannot influence responses through side channels other than replacement, and that the empty-string caveat name does not create ambiguity with custom caveat handling. No urgent security action is indicated by the diff alone.
Security signals we found
Change relaxes a uniqueness constraint that previously blocked multiple read-only middleware registrations
Read-only middlewares are now explicitly prevented from affecting message flow even if they return a replacement
No authentication, authorization, or cryptographic changes are present
No CVE, advisory, or vendor security statement is included in the commit
Evidence from the diff
In rpcperms/interceptor.go, RegisterMiddleware previously enforced a single-middleware-per-custom-caveat rule. Because read-only middlewares use an empty caveat name, attempting to register a second read-only middleware collided with the empty-string caveat and was rejected. The patch changes the duplicate check to skip the conflict when both the existing and new middlewares are read-only. In interceptMessage, the replacement logic is hardened: if a middleware is read-only but returns a replacement, the replacement is discarded with a warning rather than being propagated to the next middleware.
Changed components
rpcperms/interceptor.goInterceptorChain.RegisterMiddlewareInterceptorChain.interceptMessageInspect captured patch +18 / −9
diff --git a/rpcperms/interceptor.go b/rpcperms/interceptor.go
index 9bbef04..fc30647 100644
--- a/rpcperms/interceptor.go
+++ b/rpcperms/interceptor.go
@@ -443,9 +443,9 @@ func (r *InterceptorChain) Permissions() map[string][]bakery.Op {
// RegisterMiddleware registers a new middleware that will handle request/
// response interception for all RPC messages that are initiated with a custom
-// macaroon caveat. The name of the custom caveat a middleware is handling is
-// also its unique identifier. Only one middleware can be registered for each
-// custom caveat.
+// macaroon caveat. Only one read/write middleware can be registered for each
+// custom caveat name. Multiple read-only middlewares are permitted since they
+// cannot modify responses.
func (r *InterceptorChain) RegisterMiddleware(mw *MiddlewareHandler) error {
r.Lock()
defer r.Unlock()
@@ -457,11 +457,14 @@ func (r *InterceptorChain) RegisterMiddleware(mw *MiddlewareHandler) error {
"registered", mw.middlewareName)
}
- // For now, we only want one middleware per custom caveat name. If we
- // allowed multiple middlewares handling the same caveat there would be
- // a need for extra call chaining logic, and they could overwrite each
- // other's responses.
+ // We only want one read/write middleware per custom caveat name since
+ // multiple could overwrite each other's responses. Read-only
+ // middlewares are exempt because they cannot modify responses.
for _, middleware := range r.registeredMiddleware {
+ if middleware.readOnly && mw.readOnly {
+ continue
+ }
+
if middleware.customCaveatName == mw.customCaveatName {
return fmt.Errorf("a middleware is already registered "+
"for the custom caveat name '%s': %v",
@@ -1057,8 +1060,14 @@ func (r *InterceptorChain) interceptMessage(ctx context.Context,
// The message was replaced, make sure the next middleware in
// line receives the updated message.
- if !middleware.readOnly && resp.replace {
- currentMessage = resp.replacement
+ if resp.replace {
+ if middleware.readOnly {
+ log.Warnf("Read-only middleware %s attempted "+
+ "to replace message, ignoring",
+ middleware.middlewareName)
+ } else {
+ currentMessage = resp.replacement
+ }
}
}
Why this scored 34/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.