VYPR
High severityNVD Advisory· Published Feb 26, 2025· Updated Apr 11, 2025

Potential denial of service in golang.org/x/crypto

CVE-2025-22869

Description

SSH servers which implement file transfer protocols are vulnerable to a denial of service attack from clients which complete the key exchange slowly, or not at all, causing pending content to be read into memory, but never transmitted.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

A slow or incomplete SSH key exchange can cause servers to buffer data indefinitely, leading to a denial of service (DoS) via memory exhaustion.

Vulnerability

Detail

SSH servers that implement file transfer protocols are susceptible to a denial of service (DoS) attack where a client completes the key exchange (KEX) slowly or not at all. This causes the server to read pending content into memory, but never transmit it, potentially exhausting memory resources [1]. The issue lies in how the Golang crypto/ssh library handles the internal packet queue during the KEX handshake; packets are buffered without a size limit while waiting for KEX to finish [2].

Exploitation

An attacker can exploit this by connecting to an SSH server and intentionally delaying or aborting the key exchange process after the initial handshake. No authentication is required, as the vulnerability is triggered before the authentication phase. The attacker simply needs network access to the target SSH server. By sending data that the server queues but never forwards (because KEX is not completed), the attacker can cause the server's memory to be consumed by the accumulated pending packets [2].

Impact

Successful exploitation leads to a denial of service condition. The server's memory can be exhausted, potentially causing the SSH service to become unresponsive or crash, disrupting file transfer operations and any other services relying on the same SSH server [1]. This is a resource exhaustion vulnerability with a high severity rating.

Mitigation

The vulnerability has been patched in the Golang crypto/ssh library. The fix introduces a limit on the size of the internal packet queue while waiting for key exchange, preventing unbounded memory growth [2]. The patch is included in Go versions where the fix was applied; users are advised to update to a patched version of the golang.org/x/crypto package [4]. No workaround currently exists for unpatched systems.

AI Insight generated on May 20, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
golang.org/x/cryptoGo
< 0.35.00.35.0

Affected products

4018

Patches

1
7292932d45d5

ssh: limit the size of the internal packet queue while waiting for KEX

https://github.com/golang/cryptoNicola MurinoDec 15, 2024via ghsa
2 files changed · +257 10
  • ssh/handshake.go+37 10 modified
    @@ -25,6 +25,11 @@ const debugHandshake = false
     // quickly.
     const chanSize = 16
     
    +// maxPendingPackets sets the maximum number of packets to queue while waiting
    +// for KEX to complete. This limits the total pending data to maxPendingPackets
    +// * maxPacket bytes, which is ~16.8MB.
    +const maxPendingPackets = 64
    +
     // keyingTransport is a packet based transport that supports key
     // changes. It need not be thread-safe. It should pass through
     // msgNewKeys in both directions.
    @@ -73,11 +78,19 @@ type handshakeTransport struct {
     	incoming  chan []byte
     	readError error
     
    -	mu               sync.Mutex
    -	writeError       error
    -	sentInitPacket   []byte
    -	sentInitMsg      *kexInitMsg
    -	pendingPackets   [][]byte // Used when a key exchange is in progress.
    +	mu sync.Mutex
    +	// Condition for the above mutex. It is used to notify a completed key
    +	// exchange or a write failure. Writes can wait for this condition while a
    +	// key exchange is in progress.
    +	writeCond      *sync.Cond
    +	writeError     error
    +	sentInitPacket []byte
    +	sentInitMsg    *kexInitMsg
    +	// Used to queue writes when a key exchange is in progress. The length is
    +	// limited by pendingPacketsSize. Once full, writes will block until the key
    +	// exchange is completed or an error occurs. If not empty, it is emptied
    +	// all at once when the key exchange is completed in kexLoop.
    +	pendingPackets   [][]byte
     	writePacketsLeft uint32
     	writeBytesLeft   int64
     	userAuthComplete bool // whether the user authentication phase is complete
    @@ -134,6 +147,7 @@ func newHandshakeTransport(conn keyingTransport, config *Config, clientVersion,
     
     		config: config,
     	}
    +	t.writeCond = sync.NewCond(&t.mu)
     	t.resetReadThresholds()
     	t.resetWriteThresholds()
     
    @@ -260,6 +274,7 @@ func (t *handshakeTransport) recordWriteError(err error) {
     	defer t.mu.Unlock()
     	if t.writeError == nil && err != nil {
     		t.writeError = err
    +		t.writeCond.Broadcast()
     	}
     }
     
    @@ -363,6 +378,8 @@ write:
     			}
     		}
     		t.pendingPackets = t.pendingPackets[:0]
    +		// Unblock writePacket if waiting for KEX.
    +		t.writeCond.Broadcast()
     		t.mu.Unlock()
     	}
     
    @@ -577,11 +594,20 @@ func (t *handshakeTransport) writePacket(p []byte) error {
     	}
     
     	if t.sentInitMsg != nil {
    -		// Copy the packet so the writer can reuse the buffer.
    -		cp := make([]byte, len(p))
    -		copy(cp, p)
    -		t.pendingPackets = append(t.pendingPackets, cp)
    -		return nil
    +		if len(t.pendingPackets) < maxPendingPackets {
    +			// Copy the packet so the writer can reuse the buffer.
    +			cp := make([]byte, len(p))
    +			copy(cp, p)
    +			t.pendingPackets = append(t.pendingPackets, cp)
    +			return nil
    +		}
    +		for t.sentInitMsg != nil {
    +			// Block and wait for KEX to complete or an error.
    +			t.writeCond.Wait()
    +			if t.writeError != nil {
    +				return t.writeError
    +			}
    +		}
     	}
     
     	if t.writeBytesLeft > 0 {
    @@ -598,6 +624,7 @@ func (t *handshakeTransport) writePacket(p []byte) error {
     
     	if err := t.pushPacket(p); err != nil {
     		t.writeError = err
    +		t.writeCond.Broadcast()
     	}
     
     	return nil
    
  • ssh/handshake_test.go+220 0 modified
    @@ -539,6 +539,226 @@ func TestDisconnect(t *testing.T) {
     	}
     }
     
    +type mockKeyingTransport struct {
    +	packetConn
    +	kexInitAllowed chan struct{}
    +	kexInitSent    chan struct{}
    +}
    +
    +func (n *mockKeyingTransport) prepareKeyChange(*algorithms, *kexResult) error {
    +	return nil
    +}
    +
    +func (n *mockKeyingTransport) writePacket(packet []byte) error {
    +	if packet[0] == msgKexInit {
    +		<-n.kexInitAllowed
    +		n.kexInitSent <- struct{}{}
    +	}
    +	return n.packetConn.writePacket(packet)
    +}
    +
    +func (n *mockKeyingTransport) readPacket() ([]byte, error) {
    +	return n.packetConn.readPacket()
    +}
    +
    +func (n *mockKeyingTransport) setStrictMode() error { return nil }
    +
    +func (n *mockKeyingTransport) setInitialKEXDone() {}
    +
    +func TestHandshakePendingPacketsWait(t *testing.T) {
    +	a, b := memPipe()
    +
    +	trS := &mockKeyingTransport{
    +		packetConn:     a,
    +		kexInitAllowed: make(chan struct{}, 2),
    +		kexInitSent:    make(chan struct{}, 2),
    +	}
    +	// Allow the first KEX.
    +	trS.kexInitAllowed <- struct{}{}
    +
    +	trC := &mockKeyingTransport{
    +		packetConn:     b,
    +		kexInitAllowed: make(chan struct{}, 2),
    +		kexInitSent:    make(chan struct{}, 2),
    +	}
    +	// Allow the first KEX.
    +	trC.kexInitAllowed <- struct{}{}
    +
    +	clientConf := &ClientConfig{
    +		HostKeyCallback: InsecureIgnoreHostKey(),
    +	}
    +	clientConf.SetDefaults()
    +
    +	v := []byte("version")
    +	client := newClientTransport(trC, v, v, clientConf, "addr", nil)
    +
    +	serverConf := &ServerConfig{}
    +	serverConf.AddHostKey(testSigners["ecdsa"])
    +	serverConf.AddHostKey(testSigners["rsa"])
    +	serverConf.SetDefaults()
    +	server := newServerTransport(trS, v, v, serverConf)
    +
    +	if err := server.waitSession(); err != nil {
    +		t.Fatalf("server.waitSession: %v", err)
    +	}
    +	if err := client.waitSession(); err != nil {
    +		t.Fatalf("client.waitSession: %v", err)
    +	}
    +
    +	<-trC.kexInitSent
    +	<-trS.kexInitSent
    +
    +	// Allow and request new KEX server side.
    +	trS.kexInitAllowed <- struct{}{}
    +	server.requestKeyExchange()
    +	// Wait until the KEX init is sent.
    +	<-trS.kexInitSent
    +	// The client is not allowed to respond to the KEX, so writes will be
    +	// blocked on the server side once the packets queue is full.
    +	for i := 0; i < maxPendingPackets; i++ {
    +		p := []byte{msgRequestSuccess, byte(i)}
    +		if err := server.writePacket(p); err != nil {
    +			t.Errorf("unexpected write error: %v", err)
    +		}
    +	}
    +	// The packets queue is now full, the next write will block.
    +	server.mu.Lock()
    +	if len(server.pendingPackets) != maxPendingPackets {
    +		t.Errorf("unexpected pending packets size; got: %d, want: %d", len(server.pendingPackets), maxPendingPackets)
    +	}
    +	server.mu.Unlock()
    +
    +	writeDone := make(chan struct{})
    +	go func() {
    +		defer close(writeDone)
    +
    +		p := []byte{msgRequestSuccess, byte(65)}
    +		// This write will block until KEX completes.
    +		err := server.writePacket(p)
    +		if err != nil {
    +			t.Errorf("unexpected write error: %v", err)
    +		}
    +	}()
    +
    +	// Consume packets on the client side
    +	readDone := make(chan bool)
    +	go func() {
    +		defer close(readDone)
    +
    +		for {
    +			if _, err := client.readPacket(); err != nil {
    +				if err != io.EOF {
    +					t.Errorf("unexpected read error: %v", err)
    +				}
    +				break
    +			}
    +		}
    +	}()
    +
    +	// Allow the client to reply to the KEX and so unblock the write goroutine.
    +	trC.kexInitAllowed <- struct{}{}
    +	<-trC.kexInitSent
    +	<-writeDone
    +	// Close the client to unblock the read goroutine.
    +	client.Close()
    +	<-readDone
    +	server.Close()
    +}
    +
    +func TestHandshakePendingPacketsError(t *testing.T) {
    +	a, b := memPipe()
    +
    +	trS := &mockKeyingTransport{
    +		packetConn:     a,
    +		kexInitAllowed: make(chan struct{}, 2),
    +		kexInitSent:    make(chan struct{}, 2),
    +	}
    +	// Allow the first KEX.
    +	trS.kexInitAllowed <- struct{}{}
    +
    +	trC := &mockKeyingTransport{
    +		packetConn:     b,
    +		kexInitAllowed: make(chan struct{}, 2),
    +		kexInitSent:    make(chan struct{}, 2),
    +	}
    +	// Allow the first KEX.
    +	trC.kexInitAllowed <- struct{}{}
    +
    +	clientConf := &ClientConfig{
    +		HostKeyCallback: InsecureIgnoreHostKey(),
    +	}
    +	clientConf.SetDefaults()
    +
    +	v := []byte("version")
    +	client := newClientTransport(trC, v, v, clientConf, "addr", nil)
    +
    +	serverConf := &ServerConfig{}
    +	serverConf.AddHostKey(testSigners["ecdsa"])
    +	serverConf.AddHostKey(testSigners["rsa"])
    +	serverConf.SetDefaults()
    +	server := newServerTransport(trS, v, v, serverConf)
    +
    +	if err := server.waitSession(); err != nil {
    +		t.Fatalf("server.waitSession: %v", err)
    +	}
    +	if err := client.waitSession(); err != nil {
    +		t.Fatalf("client.waitSession: %v", err)
    +	}
    +
    +	<-trC.kexInitSent
    +	<-trS.kexInitSent
    +
    +	// Allow and request new KEX server side.
    +	trS.kexInitAllowed <- struct{}{}
    +	server.requestKeyExchange()
    +	// Wait until the KEX init is sent.
    +	<-trS.kexInitSent
    +	// The client is not allowed to respond to the KEX, so writes will be
    +	// blocked on the server side once the packets queue is full.
    +	for i := 0; i < maxPendingPackets; i++ {
    +		p := []byte{msgRequestSuccess, byte(i)}
    +		if err := server.writePacket(p); err != nil {
    +			t.Errorf("unexpected write error: %v", err)
    +		}
    +	}
    +	// The packets queue is now full, the next write will block.
    +	writeDone := make(chan struct{})
    +	go func() {
    +		defer close(writeDone)
    +
    +		p := []byte{msgRequestSuccess, byte(65)}
    +		// This write will block until KEX completes.
    +		err := server.writePacket(p)
    +		if err != io.EOF {
    +			t.Errorf("unexpected write error: %v", err)
    +		}
    +	}()
    +
    +	// Consume packets on the client side
    +	readDone := make(chan bool)
    +	go func() {
    +		defer close(readDone)
    +
    +		for {
    +			if _, err := client.readPacket(); err != nil {
    +				if err != io.EOF {
    +					t.Errorf("unexpected read error: %v", err)
    +				}
    +				break
    +			}
    +		}
    +	}()
    +
    +	// Close the server to unblock the write after an error
    +	server.Close()
    +	<-writeDone
    +	// Unblock the pending write and close the client to unblock the read
    +	// goroutine.
    +	trC.kexInitAllowed <- struct{}{}
    +	client.Close()
    +	<-readDone
    +}
    +
     func TestHandshakeRekeyDefault(t *testing.T) {
     	clientConf := &ClientConfig{
     		Config: Config{
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.