ssh: enforce the source-address critical option for all auth callbacks

CVE-2026-46595 extended source-address validation, historically applied
only to the Permissions returned by PublicKeyCallback, to the ones
returned by VerifiedPublicKeyCallback. The documented contract of
Permissions.CriticalOptions does not restrict enforcement to a specific
authentication method, so move the check to a single point at the end of
each authentication attempt, where it covers the Permissions returned by
any callback (password, keyboard-interactive, none and GSSAPI included).

The check at public key cache insertion time is kept: it remains
authoritative for the Permissions returned by PublicKeyCallback, which
VerifiedPublicKeyCallback may replace before the check at the end of the
authentication attempt runs and which are not re-checked on partial
success. It also still makes public key queries fail before the client
produces a signature when PublicKeyCallback supplies the restriction.

Fixes CVE-2026-56854
Fixes golang/go#80213

Change-Id: Ibd612a8e4240bd710e33754f3ceb95bb29169d9a
Reviewed-on: https://go-review.googlesource.com/c/crypto/+/797040
Reviewed-by: Junyang Shao <shaojunyang@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Filippo Valsorda <filippo@golang.org>
Reviewed-by: David Chase <drchase@google.com>
This commit is contained in:
Nicola Murino
2026-07-12 08:19:37 -07:00
parent 4044611da8
commit e557b08ec2
2 changed files with 126 additions and 18 deletions
+88
View File
@@ -760,6 +760,94 @@ func TestVerifiedPubKeyCallbackSourceAddress(t *testing.T) {
}
}
func TestAuthCallbacksSourceAddress(t *testing.T) {
permsWithSourceAddress := func(sourceAddress string) *Permissions {
return &Permissions{
CriticalOptions: map[string]string{
sourceAddressCriticalOption: sourceAddress,
},
}
}
methods := []struct {
name string
serverConfig func(sourceAddress string) *ServerConfig
clientAuth []AuthMethod
}{
{
name: "password",
serverConfig: func(sourceAddress string) *ServerConfig {
return &ServerConfig{
PasswordCallback: func(conn ConnMetadata, password []byte) (*Permissions, error) {
return permsWithSourceAddress(sourceAddress), nil
},
}
},
clientAuth: []AuthMethod{Password(clientPassword)},
},
{
name: "keyboard-interactive",
serverConfig: func(sourceAddress string) *ServerConfig {
return &ServerConfig{
KeyboardInteractiveCallback: func(conn ConnMetadata, challenge KeyboardInteractiveChallenge) (*Permissions, error) {
return permsWithSourceAddress(sourceAddress), nil
},
}
},
clientAuth: []AuthMethod{
KeyboardInteractive(func(name, instruction string, questions []string, echos []bool) ([]string, error) {
return nil, nil
}),
},
},
{
name: "none",
serverConfig: func(sourceAddress string) *ServerConfig {
return &ServerConfig{
NoClientAuth: true,
NoClientAuthCallback: func(conn ConnMetadata) (*Permissions, error) {
return permsWithSourceAddress(sourceAddress), nil
},
}
},
},
}
for _, method := range methods {
for _, tc := range []struct {
name string
sourceAddress string
wantErr bool
}{
{"mismatching", "192.168.99.99", true},
{"matching", "127.0.0.0/8,::1/128", false},
} {
t.Run(method.name+"/"+tc.name, func(t *testing.T) {
clientConf := &ClientConfig{
User: "user",
Auth: method.clientAuth,
HostKeyCallback: InsecureIgnoreHostKey(),
}
serverAuthErrors, err := doClientServerAuth(t, method.serverConfig(tc.sourceAddress), clientConf)
if tc.wantErr {
if err == nil {
t.Fatalf("client login succeeded with %s callback returning mismatching source-address", method.name)
}
var sourceAddressErrors int
for _, err := range serverAuthErrors {
if err != nil && strings.Contains(err.Error(), "source-address restriction") {
sourceAddressErrors++
}
}
if sourceAddressErrors == 0 {
t.Fatalf("no server auth error mentions the source-address restriction, got %v", serverAuthErrors)
}
} else if err != nil {
t.Fatalf("client login failed with matching source-address: %v", err)
}
})
}
}
}
func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {