ssh: enforce source-address critical option for all auth methods
CVE-2026-46595 (CL 781642) added the `source-address` critical-option check to the publickey path's `VerifiedPublicKeyCallback` branch in `userAuthLoop`. The other auth methods that return `*Permissions` — `none` (NoClientAuthCallback), `password` (PasswordCallback), `keyboard-interactive` (KeyboardInteractiveCallback) and `gssapi-with-mic` — still return their `Permissions` to the caller without validating the remote address against `CriticalOptions["source-address"]`.
The `Permissions` struct documentation states the package enforces the `source-address` critical option, with no caveat that this is limited to publickey. A server that sets the restriction from any non-publickey callback (password auth scoped to a corporate range, keyboard-interactive 2FA, GSSAPI SSO, or break-glass none auth) currently gets no enforcement and no warning.
This moves the enforcement to a single check after the auth-method switch so it applies uniformly to every method. The existing publickey-path checks are left in place as defense in depth.
A regression test (`TestSourceAddressCriticalOptionNonPublicKey`) covers the none/password/keyboard-interactive methods; it fails on master and passes with this change. The existing publickey control (`TestVerifiedPubKeyCallbackSourceAddress`) continues to pass.
diff --git a/ssh/server.go b/ssh/server.go
index 0192a67..31310ed 100644
--- a/ssh/server.go
+++ b/ssh/server.go
@@ -896,6 +896,20 @@
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 {
diff --git a/ssh/server_test.go b/ssh/server_test.go
index 502a25b..4cebbbd 100644
--- a/ssh/server_test.go
+++ b/ssh/server_test.go
@@ -714,6 +714,80 @@
}
}
+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 {
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
I spotted some possible problems with your PR:
1. You have a long 474 character line in the commit message body. Please add line breaks to long lines that should be wrapped. Lines in the commit message body should be wrapped at ~76 characters unless needed for things like URLs or tables. (Note: GitHub might render long lines as soft-wrapped, so double-check in the Gerrit commit message shown above.)
2. You usually need to reference a bug number for all but trivial or cosmetic fixes. For the crypto repo, the format is usually 'Fixes golang/go#12345' or 'Updates golang/go#12345' at the end of the commit message. Should you have a bug reference?
Please address any problems by updating the GitHub PR.
When complete, mark this comment as 'Done' and click the [blue 'Reply' button](https://go.dev/wiki/GerritBot#i-left-a-reply-to-a-comment-in-gerrit-but-no-one-but-me-can-see-it) above. These findings are based on heuristics; if a finding does not apply, briefly reply here saying so.
To update the commit title or commit message body shown here in Gerrit, you must edit the GitHub PR title and PR description (the first comment) in the GitHub web interface using the 'Edit' button or 'Edit' menu entry there. Note: pushing a new commit to the PR will not automatically update the commit message used by Gerrit.
For more details, see:
(In general for Gerrit code reviews, the change author is expected to [log in to Gerrit](https://go-review.googlesource.com/login/) with a Gmail or other Google account and then close out each piece of feedback by marking it as 'Done' if implemented as suggested or otherwise reply to each review comment. See the [Review](https://go.dev/doc/contribute#review) section of the Contributing Guide for details.)
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |