CVE-2023-45286 is a critical vulnerability discovered in the popular go-resty HTTP client library for Go. This flaw can cause HTTP request bodies to leak between unrelated HTTP requests, potentially exposing sensitive data to other servers. In this post, we’ll break down how this vulnerability works, show example code, and walk you through how it could be exploited.
What is go-resty?
go-resty is one of the most popular HTTP and REST clients in the Go ecosystem. It simplifies making HTTP requests, managing cookies, setting up retry logic, and marshaling/unmarshaling JSON. Because of its features, it is widely used in production applications and by open source projects.
What is CVE-2023-45286?
CVE-2023-45286 describes a race condition in go-resty that affects how HTTP request bodies are sent when retries are enabled. In certain situations, a buffer used to hold the outgoing HTTP request body (*bytes.Buffer) can be “recycled” incorrectly and reused for another, unrelated HTTP request without first being cleared.
This means part or all of a previous request’s body can end up being sent along with a new request—possibly even to a different server! If request bodies contain secrets, tokens, passwords, or user data, these could be accidentally disclosed.
The Root Cause
The bug stems from improper use of Go’s sync.Pool. sync.Pool is meant to efficiently recycle objects (here, *bytes.Buffer) to reduce memory allocations. However, if the same object is returned (via Put) to the pool multiple times, it can unexpectedly surface in a new Get call without being reset.
If a request triggers retries, the buffer might be put back into the pool more than once.
- When a new request gets a buffer from sync.Pool.Get(), it might still have data from before, because .Reset() wasn’t called.
Simplified Code Example
var bufferPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func sendRequest(client *resty.Client, body []byte) (*resty.Response, error) {
// BAD: this buffer can be 'dirty'
buf := bufferPool.Get().(*bytes.Buffer)
// BAD: missing buf.Reset() here
buf.Write(body)
// ... send request using buf ...
bufferPool.Put(buf)
}
If a retry occurs and the buffer is *Put* multiple times (without a .Reset), the following request may *Get* it “dirty”.
How Can This Be Triggered?
The vulnerability is most likely triggered when retries are enabled. If a retry happens, go-resty returns the used buffer to the pool. If it is put back multiple times (due to the way retries are handled), the buffer is not always properly cleared.
On the next request, go-resty may get that same buffer out, use it again (without a .Reset!), and append a new request body to whatever was left over. As a result, the outgoing HTTP request may have two concatenated bodies in it: the old, and the new.
Retry fires: buf1 is put back into the pool, *maybe* more than once.
3. User B sends a transaction to api2.bank.com. A buffer is grabbed from the pool: it’s still full of “AAA” from User A. Now “BBB” (User B’s data) is written to the buffer.
Here’s a proof-of-concept illustrating this class of bug, based on the vulnerability
package main
import (
"bytes"
"fmt"
"sync"
)
var bufferPool = sync.Pool{
New: func() interface{} { return new(bytes.Buffer) },
}
func fakeRequest(body string, doRetry bool) {
buf := bufferPool.Get().(*bytes.Buffer)
// If .Reset() is commented out, buffer may be "dirty"
// buf.Reset()
buf.WriteString(body)
fmt.Println("Sending request with body:", buf.String())
// Simulate retry: put buffer back multiple times
bufferPool.Put(buf)
if doRetry {
bufferPool.Put(buf) // OOPS: Put twice!
}
}
func main() {
// First request, will retry
fakeRequest("SECRET_A", true)
// Second request - if buffer is not reset, will also send previous body
fakeRequest("SECRET_B", false)
}
Output if impossible buffer reuse occurs
Sending request with body: SECRET_A
Sending request with body: SECRET_ASECRET_B // OOPS! Both secrets leaked
Note: In real life, the trigger is more complex and tied to how resty handles retries, but this illustrates the broad mechanism.
Any code using go-resty (before the patch) and enabling request retries is potentially vulnerable.
- Sensitive information (passwords, tokens, PII, etc.) can be leaked to totally unrelated destinations.
Patch
- Upgrade to the latest version of go-resty. The maintainers have released a fix that ensures buffers are always reset before reuse, and that they are only put into the pool once.
- Review your code for any use of sync.Pool + bytes.Buffer and ensure .Reset() is always called.
Consider disabling retries unless *truly* necessary.
- Sanitize and log HTTP request/response bodies carefully.
References
- GitHub Security Advisory: CVE-2023-45286
- Original Issue Discussion
- Patch Commit
- Go sync.Pool Documentation
- bytes.Buffer
Summary
CVE-2023-45286 is a severe race condition in go-resty which can spill sensitive HTTP request bodies across unrelated requests when request retries are active. This happened because a bytes.Buffer from a sync.Pool might not have been reset, so it could combine a prior request’s body with the new one. Fixes are available—upgrade your go-resty library now and audit your own buffer pooling code!
If you found this post useful, please share it with your team or fellow Go developers.
Timeline
Published on: 11/28/2023 17:15:08 UTC
Last modified on: 01/04/2024 19:15:08 UTC