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().
This commit is contained in:
xvkong233 authored and naiba committed 2026-10-01 11:17:45 +00:00
1 parent 40240ed02b
commit 3ad985f36e
12 files changed
+806 -82

No files matched your search

@@ -23,23 +23,25 @@ func prepareNATCapability(request *http.Request, natConfig *model.NAT) (natCapab
values := request.Header.Values(agentcompatcontract.IOStreamCapabilityHeader)
if len(values) == 0 {
// No capability header: legacy NAT path. Strip dashboard-issued
// Authorization values (panel JWT or panel PAT) so they never become
// origin credentials; foreign ones are ordinary request data.
stripDashboardCredential(request)
// 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.
stripDashboardCredential(request)
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 Authorization values
// are stripped, foreign ones are forwarded untouched.
stripDashboardCredential(request)
// 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")
}