Files
nezha_domains/cmd/dashboard/rpc/nat_capability_agentcompat.go
T
xvkong233 3ad985f36e fix(nat): scrub dashboard-issued credentials from all accepted token channels
The initial selective strip covered only the Authorization header, but the
dashboard also accepts its JWT via the ?token= query parameter and the
nz-jwt cookie (jwt.go TokenLookup channels). A dashboard credential arriving
through those channels would still reach a NAT backend.

Apply the same classify-then-strip rule to all three channels at NAT
ingress: values verified as dashboard-issued are removed, foreign values
are preserved byte-for-byte. Adds a ServeNAT flow test exercising the live
classifier and a wiring test pinning the gate injection in main().
2026-10-01 11:17:45 +00:00

59 lines
2.2 KiB
Go

//go:build agentcompat
package rpc
import (
"errors"
"net/http"
"github.com/nezhahq/nezha/model"
"github.com/nezhahq/nezha/pkg/agentcompatcontract"
serviceRPC "github.com/nezhahq/nezha/service/rpc"
)
type natCapabilityLease struct {
active bool
publicationOwned bool
streamLease *serviceRPC.AgentCompatNATStreamLease
access serviceRPC.AgentCompatCapabilityAccess
handle serviceRPC.AgentCompatNATPublishHandle
}
func prepareNATCapability(request *http.Request, natConfig *model.NAT) (natCapabilityLease, error) {
values := request.Header.Values(agentcompatcontract.IOStreamCapabilityHeader)
if len(values) == 0 {
// No capability header: legacy NAT path. Strip dashboard-issued
// credentials (panel JWT or panel PAT) from the Authorization header,
// the ?token= query parameter and the nz-jwt cookie so they never
// become origin credentials; foreign values are ordinary request data.
stripDashboardCredentials(request)
return natCapabilityLease{}, nil
}
request.Header.Del(agentcompatcontract.IOStreamCapabilityHeader)
if len(values) != 1 || values[0] == "" {
// Invalid capability: nothing is forwarded, but dashboard-issued
// credentials still must not survive into errors or task data.
stripDashboardCredentials(request)
return natCapabilityLease{}, errors.New("invalid NAT capability")
}
access, handle, err := serviceRPC.NezhaHandlerSingleton.ConsumeAgentCompatNATCapabilityForProfile(values[0], natConfig.ServerID, natConfig.ID)
// From here the request bytes may reach the agent (active lease) or the
// request ends in a 503; either way dashboard-issued credentials are
// stripped from every TokenLookup channel, foreign ones are forwarded
// untouched.
stripDashboardCredentials(request)
if err != nil {
return natCapabilityLease{}, errors.New("invalid NAT capability")
}
return natCapabilityLease{active: true, access: access, handle: handle}, nil
}
func (lease natCapabilityLease) cleanup(handler *serviceRPC.NezhaHandler) {
if !lease.active {
return
}
_ = handler.CancelAgentCompatIOStreamCapability(lease.access)
_ = handler.CloseAgentCompatNATStreamLease(lease.streamLease)
_ = handler.UnregisterAgentCompatIOStreamCapability(lease.access)
}