The vulnerability lies in the go-acme/lego library's failure to enforce HTTPS for all communications with the ACME Certificate Authority (CA). The root cause is in the acme/api/internal/sender.NewDoer function, which is responsible for creating a Doer object used for making HTTP requests. Prior to the patch, this function did not configure the http.Client to exclusively use HTTPS. Consequently, any ACME operation performed using the Doer's methods (Post, Get, Head) could be executed over unencrypted HTTP if the user provided an HTTP URL or if a CA returned an HTTP endpoint in its directory. This would expose sensitive information like account identifiers and request details to a network attacker.
The patch addresses this by introducing a new http.RoundTripper implementation called httpsOnly. The httpsOnly.RoundTrip method checks the scheme of every request URL and returns an error if it is not https. The sender.NewDoer function was modified to wrap the http.Client's transport with this httpsOnly round tripper, thus ensuring all subsequent requests made by the Doer are enforced to be over HTTPS. The vulnerable functions are therefore sender.NewDoer for its incorrect setup, and the sender.Doer.Post, sender.Doer.Get, and sender.Doer.Head methods, which would appear in a runtime profile when the vulnerability is triggered.
| Package Name | Ecosystem | Vulnerable Versions | First Patched Version |
|---|---|---|---|
| github.com/go-acme/lego | go | <= 4.25.1 | |
| github.com/go-acme/lego/v3 | go | <= 4.25.1 | |
| github.com/go-acme/lego/v4 | go |
package api
import (
"crypto/ecdsa"
"crypto/elliptic"
"crypto/rand"
"fmt"
"net/http"
"strings"
"testing"
"time"
"github.com/go-acme/lego/v4/acme"
)
const letsEncryptURLHTTP = "http://acme-v02.api.letsencrypt.org/directory"
const letsEncryptURLHTTPS = "https://acme-v02.api.letsencrypt.org/directory"
func changeToHTTP(url *string) {
if strings.HasPrefix(*url, "https:") {
*url = "http" + (*url)[len("https"):]
}
}
func changeToHTTPS(url *string) {
if strings.HasPrefix(*url, "http:") {
*url = "https" + (*url)[len("http"):]
}
}
func TestHTTPURLs(t *testing.T) {
privateKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
if err != nil {
t.Fatalf("error generating a private key: %v", err)
}
func() {
t.Log("testing that Discover enforces https")
_, err := New(&http.Client{
Transport: &httpsOnlyRoundTripper{inner: http.DefaultTransport},
Timeout: 20 * time.Second,
}, "", letsEncryptURLHTTP, "", privateKey)
if err != nil {
t.Errorf("New error: %v", err)
}
}()
core, err := New(&http.Client{
Transport: &httpsOnlyRoundTripper{inner: http.DefaultTransport},
Timeout: 20 * time.Second,
}, "", letsEncryptURLHTTPS, "", privateKey)
if err != nil {
t.Fatalf("New error: %v", err)
}
func() {
t.Log("testing that account creation enforces https")
// Simulate a misconfigured CA that gives out HTTP directory URLs and when
// we're done change it back to HTTPS to test the rest.
changeToHTTP(&core.directory.NewAccountURL)
defer changeToHTTPS(&core.directory.NewAccountURL)
_, err := core.Accounts.New(acme.Account{
TermsOfServiceAgreed: true,
Contact: []string{},
})
if err != nil {
t.Errorf("core.Accounts.New error: %v", err)
}
}()
_, err = core.Accounts.New(acme.Account{
TermsOfServiceAgreed: true,
Contact: []string{},
})
if err != nil {
t.Fatalf("core.Accounts.New error: %v", err)
}
}
type httpsOnlyRoundTripper struct {
inner http.RoundTripper
}
func (r *httpsOnlyRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
if req.URL.Scheme != "https" {
return nil, fmt.Errorf("non-https request is being sent")
}
return r.inner.RoundTrip(req)
}
_
| <= 4.25.1 |
| 4.25.2 |
Ongoing coverage of React2Shell