mirror of
https://github.com/Buriburizaem0n/nezha_domains.git
synced 2026-11-04 16:40:21 +00:00
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().
154 lines
5.7 KiB
Go
154 lines
5.7 KiB
Go
package rpc
|
|
|
|
import (
|
|
"net/http"
|
|
"net/url"
|
|
"testing"
|
|
|
|
"github.com/nezhahq/nezha/model"
|
|
)
|
|
|
|
// Values standing in for the credentials this dashboard issues. The fake
|
|
// gates below mirror the contract of controller.IsDashboardCredential and
|
|
// controller.IsDashboardCredentialValue; the real classification semantics —
|
|
// panel JWTs accepted regardless of expiry, foreign-signed JWTs and random
|
|
// strings rejected — are pinned by the controller package tests
|
|
// (dashboard_credential_test.go) and by the flow test that wires the real
|
|
// classifiers into ServeNAT (dashboard_credential_nat_flow_test.go).
|
|
const (
|
|
natTestDashboardJWTValue = "dashboard-signed-jwt"
|
|
natTestDashboardExpiredJWTValue = "dashboard-signed-expired-jwt"
|
|
natTestDashboardPATValue = model.APITokenPrefix + "dashboard-issued-pat"
|
|
natTestForeignJWTValue = "foreign-signed-jwt"
|
|
natTestForeignRandomValue = "foreign-random-string"
|
|
|
|
natTestDashboardJWT = "Bearer " + natTestDashboardJWTValue
|
|
natTestDashboardExpiredJWT = "Bearer " + natTestDashboardExpiredJWTValue
|
|
natTestDashboardAPIToken = "Bearer " + natTestDashboardPATValue
|
|
natTestForeignJWT = "Bearer " + natTestForeignJWTValue
|
|
natTestForeignRandom = "Bearer " + natTestForeignRandomValue
|
|
)
|
|
|
|
// installNATCredentialTestGate wires fake dashboard-credential classifiers
|
|
// for the duration of the test: exactly the dashboard-issued test values above
|
|
// are classified as dashboard credentials on every channel, everything else
|
|
// is foreign.
|
|
func installNATCredentialTestGate(t *testing.T) {
|
|
t.Helper()
|
|
originalAuthorization := natDashboardAuthorizationGate
|
|
originalValue := natDashboardCredentialValueGate
|
|
natDashboardAuthorizationGate = func(authz string) bool {
|
|
switch authz {
|
|
case natTestDashboardJWT, natTestDashboardExpiredJWT, natTestDashboardAPIToken:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
natDashboardCredentialValueGate = func(value string) bool {
|
|
switch value {
|
|
case natTestDashboardJWTValue, natTestDashboardExpiredJWTValue, natTestDashboardPATValue:
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
t.Cleanup(func() {
|
|
natDashboardAuthorizationGate = originalAuthorization
|
|
natDashboardCredentialValueGate = originalValue
|
|
})
|
|
}
|
|
|
|
func TestStripDashboardCredentialsQueryChannel(t *testing.T) {
|
|
// Given
|
|
installNATCredentialTestGate(t)
|
|
request := &http.Request{URL: &url.URL{Path: "/nat", RawQuery: "keep=1&token=" + natTestDashboardJWTValue + "&token=" + natTestForeignJWTValue + "&a=%2Fx"}}
|
|
|
|
// When
|
|
stripDashboardCredentials(request)
|
|
|
|
// Then
|
|
if got := request.URL.RawQuery; got != "keep=1&token="+natTestForeignJWTValue+"&a=%2Fx" {
|
|
t.Fatalf("query channel kept %q, want dashboard token dropped and foreign values byte-preserved", got)
|
|
}
|
|
}
|
|
|
|
func TestStripDashboardCredentialsQueryChannelEscapedValue(t *testing.T) {
|
|
// Given — a dashboard token percent-encoded in the query must still be
|
|
// recognized, while the escaped foreign element stays verbatim.
|
|
installNATCredentialTestGate(t)
|
|
request := &http.Request{URL: &url.URL{Path: "/nat", RawQuery: "token=" + url.QueryEscape(natTestDashboardJWTValue) + "&token=" + url.QueryEscape(natTestForeignJWTValue)}}
|
|
|
|
// When
|
|
stripDashboardCredentials(request)
|
|
|
|
// Then
|
|
if got := request.URL.RawQuery; got != "token="+url.QueryEscape(natTestForeignJWTValue) {
|
|
t.Fatalf("query channel kept %q, want escaped dashboard token dropped and foreign element verbatim", got)
|
|
}
|
|
}
|
|
|
|
func TestStripDashboardCredentialsCookieChannel(t *testing.T) {
|
|
// Given
|
|
installNATCredentialTestGate(t)
|
|
request := &http.Request{Header: make(http.Header)}
|
|
request.Header.Set("Cookie", "sid=abc; nz-jwt="+natTestDashboardJWTValue+"; nz-jwt="+natTestForeignJWTValue+"; theme=dark")
|
|
|
|
// When
|
|
stripDashboardCredentials(request)
|
|
|
|
// Then
|
|
if got := request.Header.Get("Cookie"); got != "sid=abc; nz-jwt="+natTestForeignJWTValue+"; theme=dark" {
|
|
t.Fatalf("cookie channel kept %q, want dashboard pair dropped and foreign pairs verbatim", got)
|
|
}
|
|
}
|
|
|
|
func TestStripDashboardCredentialsCookieChannelQuotedValue(t *testing.T) {
|
|
// Given — quoting is presentation the panel's cookie parsing strips before
|
|
// verifying, so a quoted dashboard token must be recognized.
|
|
installNATCredentialTestGate(t)
|
|
request := &http.Request{Header: make(http.Header)}
|
|
request.Header.Set("Cookie", `nz-jwt="`+natTestDashboardJWTValue+`"; sid=abc`)
|
|
|
|
// When
|
|
stripDashboardCredentials(request)
|
|
|
|
// Then
|
|
if got := request.Header.Get("Cookie"); got != "sid=abc" {
|
|
t.Fatalf("cookie channel kept %q, want quoted dashboard token dropped", got)
|
|
}
|
|
}
|
|
|
|
func TestPrepareNATCapabilityFailsClosedWithoutGate(t *testing.T) {
|
|
// Given — unwired gates: every value on every channel is treated as
|
|
// dashboard-issued and stripped, foreign ones included.
|
|
SetNATDashboardCredentialGate(nil, nil)
|
|
t.Cleanup(func() { SetNATDashboardCredentialGate(nil, nil) })
|
|
request := &http.Request{
|
|
Header: make(http.Header),
|
|
URL: &url.URL{Path: "/nat", RawQuery: "keep=1&token=" + natTestForeignJWTValue},
|
|
}
|
|
request.Header.Set("Authorization", natTestForeignRandom)
|
|
request.Header.Set("Cookie", "sid=abc; nz-jwt="+natTestForeignJWTValue)
|
|
|
|
// When
|
|
lease, err := prepareNATCapability(request, &model.NAT{Common: model.Common{ID: 91}, ServerID: 81})
|
|
|
|
// Then
|
|
if err != nil {
|
|
t.Fatalf("fail-closed NAT capability hook returned error: %v", err)
|
|
}
|
|
if lease.active {
|
|
t.Fatal("fail-closed NAT capability hook unexpectedly activated")
|
|
}
|
|
if got := request.Header.Get("Authorization"); got != "" {
|
|
t.Fatalf("fail-closed hook retained Authorization %q", got)
|
|
}
|
|
if got := request.URL.RawQuery; got != "keep=1" {
|
|
t.Fatalf("fail-closed hook kept query %q, want token parameter stripped", got)
|
|
}
|
|
if got := request.Header.Get("Cookie"); got != "sid=abc" {
|
|
t.Fatalf("fail-closed hook kept cookie %q, want nz-jwt pair stripped", got)
|
|
}
|
|
}
|