fix: CVE-2025-30204 - golang-jwt DoS vulnerability via excessive memory allocation (#3687)

* fix: CVE-2025-30204 - golang-jwt DoS vulnerability via excessive memory
allocation

Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>

* fix: linting

Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>

* chore: update project-zot/mockoidc to remove golang-jwt v3

Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>

* test: Add more tests for bearer tokens

Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>

* fix: Rewrite tests to remove MakeAuthTestServerLegacy

Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>

---------

Signed-off-by: Asgeir Nilsen <asgeir@twingine.no>
This commit is contained in:
Asgeir Storesund Nilsen
2026-01-14 10:34:58 +01:00
committed by GitHub
parent e2ba7c8e20
commit 708adf63d4
7 changed files with 831 additions and 856 deletions
-57
View File
@@ -10,7 +10,6 @@ import (
"strings"
"time"
"github.com/chartmuseum/auth"
"github.com/golang-jwt/jwt/v5"
"github.com/mitchellh/mapstructure"
@@ -86,62 +85,6 @@ func MakeAuthTestServer(serverKey, signAlg string, unauthorizedNamespace string)
return authTestServer
}
// MakeAuthTestServerLegacy makes a test HTTP server to generate bearer tokens using the github.com/chartmuseum/auth
// package, to verify backward compatibility of the token authentication process with older versions of zot.
func MakeAuthTestServerLegacy(serverKey string, unauthorizedNamespace string) *httptest.Server {
cmTokenGenerator, err := auth.NewTokenGenerator(&auth.TokenGeneratorOptions{
PrivateKeyPath: serverKey,
Audience: "Zot Registry",
Issuer: "Zot",
AddKIDHeader: true,
})
if err != nil {
panic(err)
}
authTestServer := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method != http.MethodGet {
response.WriteHeader(http.StatusMethodNotAllowed)
return
}
var access []auth.AccessEntry
scopes := request.URL.Query()["scope"]
for _, scope := range scopes {
if scope == "" {
continue
}
parts := strings.Split(scope, ":")
name := parts[1]
actions := strings.Split(parts[2], ",")
if name == unauthorizedNamespace {
actions = []string{}
}
access = append(access, auth.AccessEntry{
Name: name,
Type: "repository",
Actions: actions,
})
}
token, err := cmTokenGenerator.GenerateToken(access, time.Minute*1)
if err != nil {
panic(err)
}
response.Header().Set("Content-Type", "application/json")
fmt.Fprintf(response, `{"access_token": "%s"}`, token)
}))
return authTestServer
}
func ParseBearerAuthHeader(authHeaderRaw string) *AuthHeader {
re := regexp.MustCompile(`([a-zA-z]+)="(.+?)"`)
matches := re.FindAllStringSubmatch(authHeaderRaw, -1)
+132 -3
View File
@@ -1,8 +1,21 @@
package auth_test
import (
"context"
"crypto/rand"
"crypto/rsa"
"crypto/x509"
"crypto/x509/pkix"
"encoding/json"
"encoding/pem"
"math/big"
"net/http"
"os"
"path/filepath"
"testing"
"time"
"github.com/golang-jwt/jwt/v5"
. "github.com/smartystreets/goconvey/convey"
auth "zotregistry.dev/zot/v2/pkg/test/auth"
@@ -14,8 +27,124 @@ func TestBearerServer(t *testing.T) {
})
}
func TestBearerServerLegacy(t *testing.T) {
Convey("test MakeAuthTestServerLegacy() no serve key", t, func() {
So(func() { auth.MakeAuthTestServerLegacy("", "") }, ShouldPanic)
// doGet performs an HTTP GET request with context.
func doGet(url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, url, nil)
if err != nil {
return nil, err
}
return http.DefaultClient.Do(req)
}
// doPost performs an HTTP POST request with context.
func doPost(url string) (*http.Response, error) {
req, err := http.NewRequestWithContext(context.Background(), http.MethodPost, url, nil)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", "application/json")
return http.DefaultClient.Do(req)
}
func TestNewTokenGeneration(t *testing.T) {
Convey("test new token generation", t, func() {
tempDir := t.TempDir()
keyPath := filepath.Join(tempDir, "server.key")
certPath := filepath.Join(tempDir, "server.crt")
// Generate an RSA key pair
privateKey, err := rsa.GenerateKey(rand.Reader, 2048)
So(err, ShouldBeNil)
// Write the private key to a file
keyBytes := x509.MarshalPKCS1PrivateKey(privateKey)
keyPEM := pem.EncodeToMemory(&pem.Block{
Type: "RSA PRIVATE KEY",
Bytes: keyBytes,
})
err = os.WriteFile(keyPath, keyPEM, 0o600)
So(err, ShouldBeNil)
// Create a self-signed certificate
template := &x509.Certificate{
SerialNumber: big.NewInt(1),
Subject: pkix.Name{
CommonName: "test",
},
NotBefore: time.Now(),
NotAfter: time.Now().Add(time.Hour),
}
certDER, err := x509.CreateCertificate(
rand.Reader, template, template, &privateKey.PublicKey, privateKey,
)
So(err, ShouldBeNil)
certPEM := pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: certDER,
})
err = os.WriteFile(certPath, certPEM, 0o600)
So(err, ShouldBeNil)
Convey("new server should generate valid tokens", func() {
server := auth.MakeAuthTestServer(keyPath, "RS256", "unauthorized-repo")
defer server.Close()
resp, err := doGet(server.URL + "?scope=repository:test-repo:pull,push")
So(err, ShouldBeNil)
So(resp.StatusCode, ShouldEqual, http.StatusOK)
defer resp.Body.Close()
var tokenResp auth.AccessTokenResponse
err = json.NewDecoder(resp.Body).Decode(&tokenResp)
So(err, ShouldBeNil)
So(tokenResp.AccessToken, ShouldNotBeEmpty)
// Parse and verify the token
token, err := jwt.Parse(tokenResp.AccessToken, func(token *jwt.Token) (any, error) {
return &privateKey.PublicKey, nil
})
So(err, ShouldBeNil)
So(token.Valid, ShouldBeTrue)
So(token.Method.Alg(), ShouldEqual, "RS256")
})
Convey("new server should reject non-GET requests", func() {
server := auth.MakeAuthTestServer(keyPath, "RS256", "unauthorized-repo")
defer server.Close()
resp, err := doPost(server.URL)
So(err, ShouldBeNil)
So(resp.StatusCode, ShouldEqual, http.StatusMethodNotAllowed)
resp.Body.Close()
})
})
}
func TestParseBearerAuthHeader(t *testing.T) {
Convey("test ParseBearerAuthHeader", t, func() {
Convey("should parse valid bearer auth header", func() {
header := `Bearer realm="https://auth.example.com/token",` +
`service="registry.example.com",scope="repository:myrepo:pull"`
parsed := auth.ParseBearerAuthHeader(header)
So(parsed.Realm, ShouldEqual, "https://auth.example.com/token")
So(parsed.Service, ShouldEqual, "registry.example.com")
So(parsed.Scope, ShouldEqual, "repository:myrepo:pull")
})
Convey("should handle empty scope", func() {
header := `Bearer realm="https://auth.example.com/token",` +
`service="registry.example.com",scope=""`
parsed := auth.ParseBearerAuthHeader(header)
So(parsed.Realm, ShouldEqual, "https://auth.example.com/token")
So(parsed.Service, ShouldEqual, "registry.example.com")
So(parsed.Scope, ShouldEqual, "")
})
})
}