Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions ssh/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -896,6 +896,20 @@ userAuthLoop:
authErr = fmt.Errorf("ssh: unknown method %q", userAuthReq.Method)
}

// Enforce the "source-address" critical option for every auth
// method. CVE-2026-46595 added this check to the publickey path's
// VerifiedPublicKeyCallback branch, but the other callback-returning
// methods ("none", "password", "keyboard-interactive" and
// "gssapi-with-mic") returned their Permissions to the caller without
// validating the remote address against the restriction.
if authErr == nil && perms != nil && perms.CriticalOptions != nil {
if saco := perms.CriticalOptions[sourceAddressCriticalOption]; saco != "" {
if err := checkSourceAddress(s.RemoteAddr(), saco); err != nil {
authErr = err
}
}
}

authErrs = append(authErrs, authErr)

if config.AuthLogCallback != nil {
Expand Down
74 changes: 74 additions & 0 deletions ssh/server_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -714,6 +714,80 @@ func TestVerifiedPubKeyCallbackSourceAddress(t *testing.T) {
}
}

func TestSourceAddressCriticalOptionNonPublicKey(t *testing.T) {
// CVE-2026-46595 added source-address enforcement to the publickey
// path. The same critical option must also be enforced for the other
// auth methods that hand back *Permissions. Each callback below
// restricts access to 192.168.99.99, which never matches the loopback
// address used by netPipe, so every authentication must be rejected.
const mismatchingSourceAddr = "192.168.99.99"
restrictedPerms := func() (*Permissions, error) {
return &Permissions{
CriticalOptions: map[string]string{
sourceAddressCriticalOption: mismatchingSourceAddr,
},
}, nil
}

for _, tt := range []struct {
name string
serverConf *ServerConfig
auth []AuthMethod
}{
{
name: "none",
serverConf: &ServerConfig{
NoClientAuth: true,
NoClientAuthCallback: func(ConnMetadata) (*Permissions, error) {
return restrictedPerms()
},
},
auth: nil,
},
{
name: "password",
serverConf: &ServerConfig{
PasswordCallback: func(ConnMetadata, []byte) (*Permissions, error) {
return restrictedPerms()
},
},
auth: []AuthMethod{Password("password")},
},
{
name: "keyboard-interactive",
serverConf: &ServerConfig{
KeyboardInteractiveCallback: func(ConnMetadata, KeyboardInteractiveChallenge) (*Permissions, error) {
return restrictedPerms()
},
},
auth: []AuthMethod{KeyboardInteractive(func(string, string, []string, []bool) ([]string, error) {
return nil, nil
})},
},
} {
t.Run(tt.name, func(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
t.Fatalf("netPipe: %v", err)
}
defer c1.Close()
defer c2.Close()

tt.serverConf.AddHostKey(testSigners["rsa"])
go NewServerConn(c1, tt.serverConf)

clientConf := &ClientConfig{
User: "user",
Auth: tt.auth,
HostKeyCallback: InsecureIgnoreHostKey(),
}
if _, _, _, err := NewClientConn(c2, "", clientConf); err == nil {
t.Fatalf("client login succeeded via %s auth with a callback returning a mismatching source-address", tt.name)
}
})
}
}

func TestVerifiedPublicCallbackPartialSuccessBadUsage(t *testing.T) {
c1, c2, err := netPipe()
if err != nil {
Expand Down