High severity8.1NVD Advisory· Published Apr 4, 2017· Updated May 13, 2026
CVE-2017-3204
CVE-2017-3204
Description
The Go SSH library (x/crypto/ssh) by default does not verify host keys, facilitating man-in-the-middle attacks. Default behavior changed in commit e4e2799 to require explicitly registering a hostkey verification mechanism.
Affected packages
Versions sourced from the GitHub Security Advisory.
| Package | Affected versions | Patched versions |
|---|---|---|
golang.org/x/cryptoGo | < 0.0.0-20170330155735-e4e2799dd7aa | 0.0.0-20170330155735-e4e2799dd7aa |
Affected products
2- Go/SSH libraryv5Range: prior to commit e4e2799
Patches
1e4e2799dd7aassh: require host key checking in the ClientConfig
13 files changed · +193 −30
ssh/agent/client_test.go+3 −1 modified@@ -233,7 +233,9 @@ func TestAuth(t *testing.T) { conn.Close() }() - conf := ssh.ClientConfig{} + conf := ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } conf.Auth = append(conf.Auth, ssh.PublicKeysCallback(agent.Signers)) conn, _, _, err := ssh.NewClientConn(b, "", &conf) if err != nil {
ssh/agent/example_test.go+8 −7 modified@@ -6,20 +6,20 @@ package agent_test import ( "log" - "os" "net" + "os" - "golang.org/x/crypto/ssh" - "golang.org/x/crypto/ssh/agent" + "golang.org/x/crypto/ssh" + "golang.org/x/crypto/ssh/agent" ) func ExampleClientAgent() { // ssh-agent has a UNIX socket under $SSH_AUTH_SOCK socket := os.Getenv("SSH_AUTH_SOCK") - conn, err := net.Dial("unix", socket) - if err != nil { - log.Fatalf("net.Dial: %v", err) - } + conn, err := net.Dial("unix", socket) + if err != nil { + log.Fatalf("net.Dial: %v", err) + } agentClient := agent.NewClient(conn) config := &ssh.ClientConfig{ User: "username", @@ -29,6 +29,7 @@ func ExampleClientAgent() { // wants it. ssh.PublicKeysCallback(agentClient.Signers), }, + HostKeyCallback: ssh.InsecureIgnoreHostKey(), } sshc, err := ssh.Dial("tcp", "localhost:22", config)
ssh/agent/server_test.go+3 −1 modified@@ -56,7 +56,9 @@ func TestSetupForwardAgent(t *testing.T) { incoming <- conn }() - conf := ssh.ClientConfig{} + conf := ssh.ClientConfig{ + HostKeyCallback: ssh.InsecureIgnoreHostKey(), + } conn, chans, reqs, err := ssh.NewClientConn(b, "", &conf) if err != nil { t.Fatalf("NewClientConn: %v", err)
ssh/certs.go+1 −1 modified@@ -268,7 +268,7 @@ type CertChecker struct { // HostKeyFallback is called when CertChecker.CheckHostKey encounters a // public key that is not a certificate. It must implement host key // validation or else, if nil, all such keys are rejected. - HostKeyFallback func(addr string, remote net.Addr, key PublicKey) error + HostKeyFallback HostKeyCallback // IsRevoked is called for each certificate so that revocation checking // can be implemented. It should return true if the given certificate
ssh/client_auth_test.go+16 −3 modified@@ -92,6 +92,7 @@ func TestClientAuthPublicKey(t *testing.T) { Auth: []AuthMethod{ PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { t.Fatalf("unable to dial remote side: %s", err) @@ -104,6 +105,7 @@ func TestAuthMethodPassword(t *testing.T) { Auth: []AuthMethod{ Password(clientPassword), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -123,6 +125,7 @@ func TestAuthMethodFallback(t *testing.T) { return "WRONG", nil }), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -141,6 +144,7 @@ func TestAuthMethodWrongPassword(t *testing.T) { Password("wrong"), PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -158,6 +162,7 @@ func TestAuthMethodKeyboardInteractive(t *testing.T) { Auth: []AuthMethod{ KeyboardInteractive(answers.Challenge), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -203,6 +208,7 @@ func TestAuthMethodRSAandDSA(t *testing.T) { Auth: []AuthMethod{ PublicKeys(testSigners["dsa"], testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { t.Fatalf("client could not authenticate with rsa key: %v", err) @@ -219,6 +225,7 @@ func TestClientHMAC(t *testing.T) { Config: Config{ MACs: []string{mac}, }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { t.Fatalf("client could not authenticate with mac algo %s: %v", mac, err) @@ -254,6 +261,7 @@ func TestClientUnsupportedKex(t *testing.T) { Config: Config{ KeyExchanges: []string{"diffie-hellman-group-exchange-sha256"}, // not currently supported }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err == nil || !strings.Contains(err.Error(), "common algorithm") { t.Errorf("got %v, expected 'common algorithm'", err) @@ -273,7 +281,8 @@ func TestClientLoginCert(t *testing.T) { } clientConfig := &ClientConfig{ - User: "user", + User: "user", + HostKeyCallback: InsecureIgnoreHostKey(), } clientConfig.Auth = append(clientConfig.Auth, PublicKeys(certSigner)) @@ -363,6 +372,7 @@ func testPermissionsPassing(withPermissions bool, t *testing.T) { Auth: []AuthMethod{ PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if withPermissions { clientConfig.User = "permissions" @@ -409,6 +419,7 @@ func TestRetryableAuth(t *testing.T) { }), 2), PublicKeys(testSigners["rsa"]), }, + HostKeyCallback: InsecureIgnoreHostKey(), } if err := tryAuth(t, config); err != nil { @@ -430,7 +441,8 @@ func ExampleRetryableAuthMethod(t *testing.T) { } config := &ClientConfig{ - User: user, + HostKeyCallback: InsecureIgnoreHostKey(), + User: user, Auth: []AuthMethod{ RetryableAuthMethod(KeyboardInteractiveChallenge(Cb), NumberOfPrompts), }, @@ -450,7 +462,8 @@ func TestClientAuthNone(t *testing.T) { serverConfig.AddHostKey(testSigners["rsa"]) clientConfig := &ClientConfig{ - User: user, + User: user, + HostKeyCallback: InsecureIgnoreHostKey(), } c1, c2, err := netPipe()
ssh/client.go+49 −4 modified@@ -5,6 +5,7 @@ package ssh import ( + "bytes" "errors" "fmt" "net" @@ -68,6 +69,11 @@ func NewClient(c Conn, chans <-chan NewChannel, reqs <-chan *Request) *Client { func NewClientConn(c net.Conn, addr string, config *ClientConfig) (Conn, <-chan NewChannel, <-chan *Request, error) { fullConf := *config fullConf.SetDefaults() + if fullConf.HostKeyCallback == nil { + c.Close() + return nil, nil, nil, errors.New("ssh: must specify HostKeyCallback") + } + conn := &connection{ sshConn: sshConn{conn: c}, } @@ -173,6 +179,13 @@ func Dial(network, addr string, config *ClientConfig) (*Client, error) { return NewClient(c, chans, reqs), nil } +// HostKeyCallback is the function type used for verifying server +// keys. A HostKeyCallback must return nil if the host key is OK, or +// an error to reject it. It receives the hostname as passed to Dial +// or NewClientConn. The remote address is the RemoteAddr of the +// net.Conn underlying the the SSH connection. +type HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error + // A ClientConfig structure is used to configure a Client. It must not be // modified after having been passed to an SSH function. type ClientConfig struct { @@ -188,10 +201,12 @@ type ClientConfig struct { // be used during authentication. Auth []AuthMethod - // HostKeyCallback, if not nil, is called during the cryptographic - // handshake to validate the server's host key. A nil HostKeyCallback - // implies that all host keys are accepted. - HostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error + // HostKeyCallback is called during the cryptographic + // handshake to validate the server's host key. The client + // configuration must supply this callback for the connection + // to succeed. The functions InsecureIgnoreHostKey or + // FixedHostKey can be used for simplistic host key checks. + HostKeyCallback HostKeyCallback // ClientVersion contains the version identification string that will // be used for the connection. If empty, a reasonable default is used. @@ -209,3 +224,33 @@ type ClientConfig struct { // A Timeout of zero means no timeout. Timeout time.Duration } + +// InsecureIgnoreHostKey returns a function that can be used for +// ClientConfig.HostKeyCallback to accept any host key. It should +// not be used for production code. +func InsecureIgnoreHostKey() HostKeyCallback { + return func(hostname string, remote net.Addr, key PublicKey) error { + return nil + } +} + +type fixedHostKey struct { + key PublicKey +} + +func (f *fixedHostKey) check(hostname string, remote net.Addr, key PublicKey) error { + if f.key == nil { + return fmt.Errorf("ssh: required host key was nil") + } + if !bytes.Equal(key.Marshal(), f.key.Marshal()) { + return fmt.Errorf("ssh: host key mismatch") + } + return nil +} + +// FixedHostKey returns a function for use in +// ClientConfig.HostKeyCallback to accept only a specific host key. +func FixedHostKey(key PublicKey) HostKeyCallback { + hk := &fixedHostKey{key} + return hk.check +}
ssh/client_test.go+42 −0 modified@@ -6,13 +6,15 @@ package ssh import ( "net" + "strings" "testing" ) func testClientVersion(t *testing.T, config *ClientConfig, expected string) { clientConn, serverConn := net.Pipe() defer clientConn.Close() receivedVersion := make(chan string, 1) + config.HostKeyCallback = InsecureIgnoreHostKey() go func() { version, err := readVersion(serverConn) if err != nil { @@ -37,3 +39,43 @@ func TestCustomClientVersion(t *testing.T) { func TestDefaultClientVersion(t *testing.T) { testClientVersion(t, &ClientConfig{}, packageVersion) } + +func TestHostKeyCheck(t *testing.T) { + for _, tt := range []struct { + name string + wantError string + key PublicKey + }{ + {"no callback", "must specify HostKeyCallback", nil}, + {"correct key", "", testSigners["rsa"].PublicKey()}, + {"mismatch", "mismatch", testSigners["ecdsa"].PublicKey()}, + } { + c1, c2, err := netPipe() + if err != nil { + t.Fatalf("netPipe: %v", err) + } + defer c1.Close() + defer c2.Close() + serverConf := &ServerConfig{ + NoClientAuth: true, + } + serverConf.AddHostKey(testSigners["rsa"]) + + go NewServerConn(c1, serverConf) + clientConf := ClientConfig{ + User: "user", + } + if tt.key != nil { + clientConf.HostKeyCallback = FixedHostKey(tt.key) + } + + _, _, _, err = NewClientConn(c2, "", &clientConf) + if err != nil { + if tt.wantError == "" || !strings.Contains(err.Error(), tt.wantError) { + t.Errorf("%s: got error %q, missing %q", err.Error(), tt.wantError) + } + } else if tt.wantError != "" { + t.Errorf("%s: succeeded, but want error string %q", tt.name, tt.wantError) + } + } +}
ssh/doc.go+3 −0 modified@@ -14,5 +14,8 @@ others. References: [PROTOCOL.certkeys]: http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.certkeys?rev=HEAD [SSH-PARAMETERS]: http://www.iana.org/assignments/ssh-parameters/ssh-parameters.xml#ssh-parameters-1 + +This package does not fall under the stability promise of the Go language itself, +so its API may be changed when pressing needs arise. */ package ssh // import "golang.org/x/crypto/ssh"
ssh/example_test.go+54 −3 modified@@ -5,12 +5,16 @@ package ssh_test import ( + "bufio" "bytes" "fmt" "io/ioutil" "log" "net" "net/http" + "os" + "path/filepath" + "strings" "golang.org/x/crypto/ssh" "golang.org/x/crypto/ssh/terminal" @@ -90,8 +94,6 @@ func ExampleNewServerConn() { // The incoming Request channel must be serviced. go ssh.DiscardRequests(reqs) - // Service the incoming Channel channel. - // Service the incoming Channel channel. for newChannel := range chans { // Channels have a type, depending on the application level @@ -131,16 +133,59 @@ func ExampleNewServerConn() { } } +func ExampleHostKeyCheck() { + // Every client must provide a host key check. Here is a + // simple-minded parse of OpenSSH's known_hosts file + host := "hostname" + file, err := os.Open(filepath.Join(os.Getenv("HOME"), ".ssh", "known_hosts")) + if err != nil { + log.Fatal(err) + } + defer file.Close() + + scanner := bufio.NewScanner(file) + var hostKey ssh.PublicKey + for scanner.Scan() { + fields := strings.Split(scanner.Text(), " ") + if len(fields) != 3 { + continue + } + if strings.Contains(fields[0], host) { + var err error + hostKey, _, _, _, err = ssh.ParseAuthorizedKey(scanner.Bytes()) + if err != nil { + log.Fatalf("error parsing %q: %v", fields[2], err) + } + break + } + } + + if hostKey == nil { + log.Fatalf("no hostkey for %s", host) + } + + config := ssh.ClientConfig{ + User: os.Getenv("USER"), + HostKeyCallback: ssh.FixedHostKey(hostKey), + } + + _, err = ssh.Dial("tcp", host+":22", &config) + log.Println(err) +} + func ExampleDial() { + var hostKey ssh.PublicKey // An SSH client is represented with a ClientConn. // // To authenticate with the remote server you must pass at least one - // implementation of AuthMethod via the Auth field in ClientConfig. + // implementation of AuthMethod via the Auth field in ClientConfig, + // and provide a HostKeyCallback. config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("yourpassword"), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } client, err := ssh.Dial("tcp", "yourserver.com:22", config) if err != nil { @@ -166,6 +211,7 @@ func ExampleDial() { } func ExamplePublicKeys() { + var hostKey ssh.PublicKey // A public key may be used to authenticate against the remote // server by using an unencrypted PEM-encoded private key file. // @@ -188,6 +234,7 @@ func ExamplePublicKeys() { // Use the PublicKeys method for remote authentication. ssh.PublicKeys(signer), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } // Connect to the remote server and perform the SSH handshake. @@ -199,11 +246,13 @@ func ExamplePublicKeys() { } func ExampleClient_Listen() { + var hostKey ssh.PublicKey config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } // Dial your ssh server. conn, err := ssh.Dial("tcp", "localhost:22", config) @@ -226,12 +275,14 @@ func ExampleClient_Listen() { } func ExampleSession_RequestPty() { + var hostKey ssh.PublicKey // Create client config config := &ssh.ClientConfig{ User: "username", Auth: []ssh.AuthMethod{ ssh.Password("password"), }, + HostKeyCallback: ssh.FixedHostKey(hostKey), } // Connect to ssh server conn, err := ssh.Dial("tcp", "localhost:22", config)
ssh/handshake.go+4 −6 modified@@ -74,7 +74,7 @@ type handshakeTransport struct { startKex chan *pendingKex // data for host key checking - hostKeyCallback func(hostname string, remote net.Addr, key PublicKey) error + hostKeyCallback HostKeyCallback dialAddress string remoteAddr net.Addr @@ -614,11 +614,9 @@ func (t *handshakeTransport) client(kex kexAlgorithm, algs *algorithms, magics * return nil, err } - if t.hostKeyCallback != nil { - err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey) - if err != nil { - return nil, err - } + err = t.hostKeyCallback(t.dialAddress, t.remoteAddr, hostKey) + if err != nil { + return nil, err } return result, nil
ssh/handshake_test.go+1 −0 modified@@ -436,6 +436,7 @@ func testHandshakeErrorHandlingN(t *testing.T, readLimit, writeLimit int, couple clientConf.SetDefaults() clientConn := newHandshakeTransport(&errorKeyingTransport{b, -1, -1}, &clientConf, []byte{'a'}, []byte{'b'}) clientConn.hostKeyAlgorithms = []string{key.PublicKey().Type()} + clientConn.hostKeyCallback = InsecureIgnoreHostKey() go clientConn.readLoop() go clientConn.kexLoop()
ssh/session_test.go+7 −3 modified@@ -59,7 +59,8 @@ func dial(handler serverType, t *testing.T) *Client { }() config := &ClientConfig{ - User: "testuser", + User: "testuser", + HostKeyCallback: InsecureIgnoreHostKey(), } conn, chans, reqs, err := NewClientConn(c2, "", config) @@ -641,7 +642,8 @@ func TestSessionID(t *testing.T) { } serverConf.AddHostKey(testSigners["ecdsa"]) clientConf := &ClientConfig{ - User: "user", + HostKeyCallback: InsecureIgnoreHostKey(), + User: "user", } go func() { @@ -747,7 +749,9 @@ func TestHostKeyAlgorithms(t *testing.T) { // By default, we get the preferred algorithm, which is ECDSA 256. - clientConf := &ClientConfig{} + clientConf := &ClientConfig{ + HostKeyCallback: InsecureIgnoreHostKey(), + } connect(clientConf, KeyAlgoECDSA256) // Client asks for RSA explicitly.
ssh/test/cert_test.go+2 −1 modified@@ -36,7 +36,8 @@ func TestCertLogin(t *testing.T) { } conf := &ssh.ClientConfig{ - User: username(), + User: username(), + HostKeyCallback: ssh.InsecureIgnoreHostKey(), } conf.Auth = append(conf.Auth, ssh.PublicKeys(certSigner)) client, err := s.TryDial(conf)
Vulnerability mechanics
Generated by null/stub on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.
References
15- github.com/golang/crypto/commit/e4e2799dd7aab89f583e1d898300d96367750991nvdPatchThird Party AdvisoryWEB
- www.securityfocus.com/bid/97481nvdThird Party AdvisoryVDB Entry
- bridge.grumpy-troll.org/2017/04/golang-ssh-security/nvdThird Party Advisory
- github.com/advisories/GHSA-xhjq-w7xm-p8qjghsaADVISORY
- github.com/golang/go/issues/19767nvdThird Party AdvisoryWEB
- godoc.org/golang.org/x/crypto/sshnvdVendor AdvisoryWEB
- nvd.nist.gov/vuln/detail/CVE-2017-3204ghsaADVISORY
- bridge.grumpy-troll.org/2017/04/golang-ssh-securityghsaWEB
- go.dev/cl/340830ghsaWEB
- go.dev/cl/38701ghsaWEB
- go.dev/issue/19767ghsaWEB
- go.googlesource.com/crypto/+/e4e2799dd7aab89f583e1d898300d96367750991ghsaWEB
- pkg.go.dev/vuln/GO-2020-0013ghsaWEB
- web.archive.org/web/20170423080311/https://www.securityfocus.com/bid/97481ghsaWEB
- web.nvd.nist.gov/view/vuln/detailghsaWEB
News mentions
0No linked articles in our index yet.