Information disclosure / Metadata leakage

HIGH
kubernetes/kubernetes
Commit: e8742a6ba73f
Affected: v1.36.0-beta.0
2026-05-26 19:28 UTC

Description

The commit fixes a vulnerability where resources could leak or inherit sensitive system metadata when created via update (create-via-update) or create via apply. Specifically, during update-based creation paths, the API server could carry over or reflect system fields such as UID, CreationTimestamp, SelfLink, DeletionTimestamp, and DeletionGracePeriodSeconds from the input ObjectMeta, or from an existing resource, into the newly created object. The patch wipes these system fields before finalizing the creation, ensuring that new objects receive fresh identity information and do not disclose potentially sensitive metadata from an existing resource or from the payload. The accompanying test suite asserts that after creation via POST, PUT (create-via-update), and SSA apply, these fields are cleared, and new governance metadata is assigned by the server.

Proof of Concept

PoC (conceptual and executable steps): - Prerequisites: A Kubernetes cluster using a version affected by this patch (e.g., v1.36.0-beta.0) and a resource type that supports create-on-update (allowCreateOnUpdate) for its strategy (e.g., Leases in tests). - Step 1: Create a resource with known system metadata to use as a source of leakage (optional for demonstration): kubectl create namespace demo; kubectl -n demo create configmap leak-demo --from-literal=x=y Note the UID, CreationTimestamp, and SelfLink of this resource. - Step 2: Attempt to create a new resource via the update path (create-on-update) and inject crafted metadata that mirrors the leaking fields (e.g., a specific UID and CreationTimestamp) in the request body. This demonstrates the create-via-update path carrying over system fields if not sanitized. Example REST operation (conceptual): curl -k -X PUT https://<k8s-api>/api/v1/namespaces/<ns>/configmaps/<name> \ -H 'Content-Type: application/json' \ -d '{ "apiVersion": "v1", "kind": "ConfigMap", "metadata": { "name": "malicious-create", "uid": "00000000-0000-0000-0000-000000000001", "creationTimestamp": "1999-01-01T00:00:00Z", "selfLink": "/this/should/be/wiped", "deletionTimestamp": "2020-01-01T00:00:00Z", "deletionGracePeriodSeconds": 30 } }' - Step 3: Observe the response/resource and verify that the created object has UID, CreationTimestamp, SelfLink, DeletionTimestamp, and DeletionGracePeriodSeconds exactly as provided in the payload (information leakage / identity spoofing possible). - Step 4: Apply the fix by wiping system fields as implemented in the patch, then re-do Step 2. After the fix, the server should assign a new UID and a fresh CreationTimestamp, and SelfLink/Deletion fields should be cleared as part of the create-via-update flow. Verification can be done by inspecting the created object (kubectl getConfigMap malicious-create -o yaml) and confirming the fields above are regenerated/cleared. Reference: The patch explicitly calls rest.WipeObjectMetaSystemFields(objectMeta) before FillObjectMetaSystemFields in the Update path where create-via-update and create-via-apply are handled, and the tests added in metadata_test.go verify that UID, CreationTimestamp, SelfLink, DeletionTimestamp, and DeletionGracePeriodSeconds are cleared on creation via these paths.

Commit Details

Author: Kubernetes Prow Robot

Date: 2026-05-08 20:27 UTC

Message:

Merge pull request #138908 from jpbetz/fix-create-via-update-metdata-wiping Fix create via update metdata wiping

Triage Assessment

Vulnerability Type: Information disclosure / Metadata leakage

Confidence: HIGH

Reasoning:

The patch wipes system metadata (UID, CreationTimestamp, SelfLink, DeletionTimestamp, DeletionGracePeriodSeconds) for create-via-update and create-via-apply paths to ensure new objects have fresh identity and don't inadvertently carry over potentially sensitive metadata from an existing resource. This prevents metadata leakage or misuse that could lead to inconsistent identity and authorization behavior when an object is created via an update flow.

Verification Assessment

Vulnerability Type: Information disclosure / Metadata leakage

Confidence: HIGH

Affected Versions: v1.36.0-beta.0

Code Diff

diff --git a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go index e77c10960e227..88cabfc42c881 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/generic/registry/store.go @@ -678,6 +678,10 @@ func (e *Store) Update(ctx context.Context, name string, objInfo rest.UpdatedObj if objectMeta, err := meta.Accessor(obj); err != nil { return nil, nil, err } else { + // Wipe metadata on create-via-update and create-via-apply + // requests to match create behavior. Note that this happens + // AFTER preconditions are checked. + rest.WipeObjectMetaSystemFields(objectMeta) rest.FillObjectMetaSystemFields(objectMeta) } diff --git a/test/integration/apiserver/metadata_test.go b/test/integration/apiserver/metadata_test.go new file mode 100644 index 0000000000000..6c1800d04ee39 --- /dev/null +++ b/test/integration/apiserver/metadata_test.go @@ -0,0 +1,164 @@ +/* +Copyright The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package apiserver + +import ( + "fmt" + "testing" + "time" + + coordinationv1 "k8s.io/api/coordination/v1" + corev1 "k8s.io/api/core/v1" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/kubernetes/test/integration/framework" + "k8s.io/utils/ptr" +) + +// TestCreateMetadataWiping tests that metadata wiping works as expected on all operations +// that can create a resource. This includes creates-via-update requests allowed for strategies +// that support AllowCreateOnUpdate. +func TestCreateMetadataWiping(t *testing.T) { + ctx, client, _, tearDown := setup(t) + defer tearDown() + + ns := framework.CreateNamespaceOrDie(client, "create-metadata-wiping", t) + defer framework.DeleteNamespaceOrDie(client, ns, t) + + uidValue := types.UID("00000000-0000-0000-0000-000000000000") + creationValue := metav1.NewTime(time.Date(1999, 1, 1, 0, 0, 0, 0, time.UTC)) + selfLinkValue := "/this/should/be/wiped" + deletionValue := metav1.NewTime(time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC)) + gracePeriodValue := int64(30) + + assertCleared := func(t *testing.T, m metav1.Object) { + t.Helper() + if got := m.GetUID(); got == "" || got == uidValue { + t.Errorf("UID not regenerated on create: got %q, input was %q", got, uidValue) + } + if got := m.GetCreationTimestamp(); got.IsZero() || got.Equal(&creationValue) { + t.Errorf("CreationTimestamp not regenerated on create: got %v, input was %v", got.UTC(), creationValue.UTC()) + } + if got := m.GetSelfLink(); got != "" { + t.Errorf("SelfLink not cleared on create: got %q, want empty", got) + } + if got := m.GetDeletionTimestamp(); got != nil { + t.Errorf("DeletionTimestamp not cleared on create: got %v, want nil", got.UTC()) + } + if got := m.GetDeletionGracePeriodSeconds(); got != nil { + t.Errorf("DeletionGracePeriodSeconds not cleared on create: got %d, want nil", *got) + } + } + + t.Run("POST create", func(t *testing.T) { + cm := &corev1.ConfigMap{ + ObjectMeta: metav1.ObjectMeta{ + Name: "post-create", + UID: uidValue, + CreationTimestamp: creationValue, + SelfLink: selfLinkValue, + DeletionTimestamp: &deletionValue, + DeletionGracePeriodSeconds: &gracePeriodValue, + }, + } + got, err := client.CoreV1().ConfigMaps(ns.Name).Create(ctx, cm, metav1.CreateOptions{}) + if err != nil { + t.Fatalf("POST create failed: %v", err) + } + assertCleared(t, got) + }) + + t.Run("PUT create-via-update", func(t *testing.T) { + // UID and ResourceVersion are omitted here as they are not unconditionally wiped. They + // are used precondition checks, which are tested separtely. + lease := &coordinationv1.Lease{ + ObjectMeta: metav1.ObjectMeta{ + Name: "put-create", + Namespace: ns.Name, + CreationTimestamp: creationValue, + SelfLink: selfLinkValue, + DeletionTimestamp: &deletionValue, + DeletionGracePeriodSeconds: &gracePeriodValue, + }, + Spec: coordinationv1.LeaseSpec{ + HolderIdentity: ptr.To("metadata-test"), + }, + } + got, err := client.CoordinationV1().Leases(ns.Name).Update(ctx, lease, metav1.UpdateOptions{}) + if err != nil { + t.Fatalf("PUT create-via-update failed: %v", err) + } + assertCleared(t, got) + }) + + t.Run("PATCH Apply create)", func(t *testing.T) { + body := fmt.Sprintf(`{ + "apiVersion": "v1", + "kind": "ConfigMap", + "metadata": { + "name": "ssa-create", + "namespace": %q, + "creationTimestamp": %q, + "selfLink": %q, + "deletionTimestamp": %q, + "deletionGracePeriodSeconds": 30 + } + }`, ns.Name, creationValue.UTC().Format(time.RFC3339), selfLinkValue, deletionValue.UTC().Format(time.RFC3339)) + + result, err := client.CoreV1().RESTClient().Patch(types.ApplyPatchType). + Namespace(ns.Name). + Resource("configmaps"). + Name("ssa-create"). + Param("fieldManager", "metadata-test"). + Body([]byte(body)). + Do(ctx).Get() + if err != nil { + t.Fatalf("SSA apply create failed: %v", err) + } + got, ok := result.(*corev1.ConfigMap) + if !ok { + t.Fatalf("expected *ConfigMap, got %T", result) + } + assertCleared(t, got) + }) + + // All other patch operations are NOT allowed to create via patch. + + patchCases := []struct { + name string + patchType types.PatchType + body []byte + }{ + {"json", types.JSONPatchType, []byte(`[{"op":"add","path":"/data","value":{"k":"v"}}]`)}, + {"merge", types.MergePatchType, []byte(`{"data":{"k":"v"}}`)}, + {"strategic-merge", types.StrategicMergePatchType, []byte(`{"data":{"k":"v"}}`)}, + } + for _, tc := range patchCases { + t.Run("PATCH "+tc.name+" create", func(t *testing.T) { + err := client.CoreV1().RESTClient().Patch(tc.patchType). + Namespace(ns.Name). + Resource("configmaps"). + Name("missing-" + tc.name). + Body(tc.body). + Do(ctx).Error() + if !apierrors.IsNotFound(err) { + t.Errorf("expected NotFound from %s patch on missing object, got %v", tc.name, err) + } + }) + } +}
← Back to Alerts View on GitHub →