Input validation / Configuration validation

MEDIUM
kubernetes/kubernetes
Commit: 30c76c18798e
Affected: <= v1.36.0-beta.0
2026-05-26 18:37 UTC

Description

The commit integrates declarative validation into REST create/update strategies by merging declarative validation with existing handwritten validation. Specifically, it updates validation flow so that when a strategy implements DeclarativeValidationStrategy, the runtime will first run handwritten validation (Validate/ValidateUpdate), then run declarative validation (ValidateDeclaratively) and merge the results, performing migration checks. It also wires declarative validation into BeforeCreate/BeforeUpdate paths and introduces configuration hooks (DeclarativeValidationConfigurer, DeclarativeValidationConfig) to tailor declarative validation per strategy. This reduces the risk that invalid configurations bypass API boundary validation, strengthening input/configuration validation and policy enforcement at REST boundaries. The change is a defensive hardening of input validation rather than a user-facing feature, and it affects internal API server validation flows across create and update operations. A real vulnerability analogous to this change would be a scenario where declarative validation could run independently of handwritten validation, or where its errors were not merged with handwritten validation errors, allowing invalid configurations to be accepted if only one validation path fired. By ensuring declarative and handwritten validations are merged, this patch mitigates that risk and tightens validation coverage across API boundaries. Affected behavior summary: - Before: If a strategy implemented DeclarativeValidationStrategy, declarative validation might not be consistently applied in conjunction with handwritten validation during create/update flows. - After: For create/update operations, handwritten validation results are computed and then declarative validation is invoked (when applicable), with merged errors returned to the caller. This ensures both validation sources contribute to the final decision. Security posture impact: improves input/configuration validation coverage, reducing chances of misconfigurations or invalid resources slipping through API validation. It does not introduce a new exposure and is aimed at reducing a potential validation bypass vector.

Proof of Concept

Proof-of-concept outline (illustrative, not executed): Goal: Demonstrate how the patch changes validation semantics by merging declarative validation with handwritten validation during resource creation. 1) Define a minimal strategy that implements both handwritten and declarative validation: - Validate(ctx, obj) returns no errors (handwritten validation passes). - ValidateDeclaratively(ctx, obj, old, errs, opType, config) checks a declarative rule (for example, requiring a string field to be all lowercase) and appends an error if violated. 2) Create a sample object that would violate the declarative rule but pass handwritten validation: - obj := Foo{ Spec: FooSpec{ Name: "Bar" } } - handwrittenErrs := strategy.Validate(ctx, obj) // returns [] (no errors) - mergedErrs := strategy.ValidateDeclaratively(ctx, obj, nil, handwrittenErrs, operation.Create, config) // returns an error: spec.name must be lowercase 3) Before this patch, the API path would only consider handwritten validation when producing the Admission/Creation error (or would not merge declarative errors in some pathways depending on implementation). - In such a scenario, creating obj with Name "Bar" would be accepted if handwritten validation passed. 4) After this patch, the API path explicitly merges declarative validation results with handwritten results, causing the operation to be rejected if the declarative rule is violated. Minimal illustrative Go-like snippet (conceptual, not production-ready): // Pseudo-types to illustrate the concept type FooStrategy struct{ } func (s *FooStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { return field.ErrorList{} } func (s *FooStrategy) ValidateDeclaratively(ctx context.Context, obj runtime.Object, old runtime.Object, errs field.ErrorList, opType operation.Type, config DeclarativeValidationConfig) field.ErrorList { fo := obj.(*Foo) if fo.Spec.Name != strings.ToLower(fo.Spec.Name) { return append(errs, field.Invalid(field.NewPath("spec", "name"), fo.Spec.Name, "name must be lowercase")) } return errs } ctx := context.Background() obj := &Foo{Spec: FooSpec{Name: "Bar"}} strategy := &FooStrategy{} handwritten := strategy.Validate(ctx, obj) // empty merged := strategy.ValidateDeclaratively(ctx, obj, nil, handwritten, operation.Create, DeclarativeValidationConfig{}) // contains error for uppercase name if len(merged) > 0 { // creation would be rejected } Note: The PoC is illustrative and focuses on the interaction between handwritten and declarative validations as affected by this patch. In real Kubernetes code, the declarative validation path is wired via DeclarativeValidationStrategy and DeclarativeValidationConfigurer interfaces, and the opType parameter would be operation.Create. The key exploit path being demonstrated is: without this patch, a resource with a violation detectable by declarative validation could slip through due to not merging declarative errors with handwritten errors; with the patch, such errors are merged and cause rejection.

Commit Details

Author: Joe Betz

Date: 2026-04-10 19:58 UTC

Message:

Make declarative validation part of strategies

Triage Assessment

Vulnerability Type: Input validation / Configuration validation

Confidence: MEDIUM

Reasoning:

The commit enhances integration of declarative validation into REST strategies, merging declarative validation with existing (handwritten) validation during create/update paths. This strengthens input validation and policy enforcement at API boundaries, reducing chances of invalid or insecure resources slipping through. While not pointing to a specific named vulnerability, it improves validation coverage and migration checks, which can mitigate vulnerability classes like invalid input handling and misconfigurations.

Verification Assessment

Vulnerability Type: Input validation / Configuration validation

Confidence: MEDIUM

Affected Versions: <= v1.36.0-beta.0

Code Diff

diff --git a/pkg/api/testing/validation.go b/pkg/api/testing/validation.go index 69ad77caec4ee..d6ce1051065a4 100644 --- a/pkg/api/testing/validation.go +++ b/pkg/api/testing/validation.go @@ -23,6 +23,7 @@ import ( "strconv" "testing" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" runtimetest "k8s.io/apimachinery/pkg/runtime/testing" @@ -39,12 +40,6 @@ import ( "sigs.k8s.io/randfill" ) -// ValidateFunc is a function that runs validation. -type ValidateFunc func(ctx context.Context, obj runtime.Object) field.ErrorList - -// ValidateUpdateFunc is a function that runs update validation. -type ValidateUpdateFunc func(ctx context.Context, obj, old runtime.Object) field.ErrorList - // VerifyVersionedValidationEquivalence tests that all versions of an API return equivalent validation errors. // It accepts optional configuration to handle path normalization across API versions where structures differ. func VerifyVersionedValidationEquivalence(t *testing.T, obj, old runtime.Object, testConfigs ...ValidationTestConfig) { @@ -286,7 +281,7 @@ func WithMinEmulationVersion(v *version.Version) ValidationTestConfig { // guaranteeing a safe migration. It also checks the errors against an expected set. // It compares errors by field, origin and type; all three should match to be called equivalent. // It also make sure all versions of the given API returns equivalent errors. -func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime.Object, validateFn ValidateFunc, expectedErrs field.ErrorList, testConfigs ...ValidationTestConfig) { +func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime.Object, strategy rest.RESTCreateStrategy, expectedErrs field.ErrorList, testConfigs ...ValidationTestConfig) { t.Helper() opts := &validationOption{} for _, testcfg := range testConfigs { @@ -294,7 +289,15 @@ func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime. } verifyValidationEquivalence(t, expectedErrs, func(c context.Context) field.ErrorList { - return validateFn(c, obj) + errs := strategy.Validate(c, obj) + if dv, ok := strategy.(rest.DeclarativeValidationStrategy); ok { + var config rest.DeclarativeValidationConfig + if vc, ok := strategy.(rest.DeclarativeValidationConfigurer); ok { + config = vc.DeclarativeValidationConfig(c, obj, nil) + } + errs = dv.ValidateDeclaratively(c, obj, nil, errs, operation.Create, config) + } + return errs }, ctx, opts) VerifyVersionedValidationEquivalence(t, obj, nil, testConfigs...) } @@ -317,7 +320,7 @@ func VerifyValidationEquivalence(t *testing.T, ctx context.Context, obj runtime. // guaranteeing a safe migration. It also checks the errors against an expected set. // It compares errors by field, origin and type; all three should match to be called equivalent. // It also make sure all versions of the given API returns equivalent errors. -func VerifyUpdateValidationEquivalence(t *testing.T, ctx context.Context, obj, old runtime.Object, validateUpdateFn ValidateUpdateFunc, expectedErrs field.ErrorList, testConfigs ...ValidationTestConfig) { +func VerifyUpdateValidationEquivalence(t *testing.T, ctx context.Context, obj, old runtime.Object, strategy rest.RESTUpdateStrategy, expectedErrs field.ErrorList, testConfigs ...ValidationTestConfig) { t.Helper() opts := &validationOption{} for _, testcfg := range testConfigs { @@ -325,7 +328,15 @@ func VerifyUpdateValidationEquivalence(t *testing.T, ctx context.Context, obj, o } verifyValidationEquivalence(t, expectedErrs, func(c context.Context) field.ErrorList { - return validateUpdateFn(c, obj, old) + errs := strategy.ValidateUpdate(c, obj, old) + if dv, ok := strategy.(rest.DeclarativeValidationStrategy); ok { + var config rest.DeclarativeValidationConfig + if vc, ok := strategy.(rest.DeclarativeValidationConfigurer); ok { + config = vc.DeclarativeValidationConfig(c, obj, old) + } + errs = dv.ValidateDeclaratively(c, obj, old, errs, operation.Update, config) + } + return errs }, ctx, opts) VerifyVersionedValidationEquivalence(t, obj, old, testConfigs...) } diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go index 87b2c9d81bfdb..8755f60a341bd 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/create.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/operation" "k8s.io/apimachinery/pkg/api/validate/content" genericvalidation "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -126,7 +127,11 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime. strategy.PrepareForCreate(ctx, obj) - if errs := strategy.Validate(ctx, obj); len(errs) > 0 { + errs := strategy.Validate(ctx, obj) + if dv, ok := strategy.(DeclarativeValidationStrategy); ok { + errs = dv.ValidateDeclaratively(ctx, obj, nil, errs, operation.Create, declarativeValidationOptions(ctx, strategy, obj)) + } + if len(errs) > 0 { return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs) } @@ -146,6 +151,14 @@ func BeforeCreate(strategy RESTCreateStrategy, ctx context.Context, obj runtime. return nil } +func declarativeValidationOptions(ctx context.Context, strategy RESTCreateStrategy, obj runtime.Object) DeclarativeValidationConfig { + var config DeclarativeValidationConfig + if vc, ok := strategy.(DeclarativeValidationConfigurer); ok { + config = vc.DeclarativeValidationConfig(ctx, obj, nil) + } + return config +} + // CheckGeneratedNameError checks whether an error that occurred creating a resource is due // to generation being unable to pick a valid name. func CheckGeneratedNameError(ctx context.Context, strategy RESTCreateStrategy, err error, obj runtime.Object) error { diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go index 32de351aa7ae9..d2b4f68f3ba88 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/update.go @@ -22,6 +22,7 @@ import ( "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" + "k8s.io/apimachinery/pkg/api/operation" genericvalidation "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" @@ -151,6 +152,9 @@ func BeforeUpdate(strategy RESTUpdateStrategy, ctx context.Context, obj, old run } errs = append(errs, strategy.ValidateUpdate(ctx, obj, old)...) + if dv, ok := strategy.(DeclarativeValidationStrategy); ok { + errs = dv.ValidateDeclaratively(ctx, obj, old, errs, operation.Update, declarativeValidationUpdateOptions(ctx, strategy, obj, old)) + } if len(errs) > 0 { RecordDuplicateValidationErrors(ctx, kind.GroupKind(), errs) return errors.NewInvalid(kind.GroupKind(), objectMeta.GetName(), errs) @@ -165,6 +169,14 @@ func BeforeUpdate(strategy RESTUpdateStrategy, ctx context.Context, obj, old run return nil } +func declarativeValidationUpdateOptions(ctx context.Context, strategy RESTUpdateStrategy, obj, old runtime.Object) DeclarativeValidationConfig { + var config DeclarativeValidationConfig + if vc, ok := strategy.(DeclarativeValidationConfigurer); ok { + config = vc.DeclarativeValidationConfig(ctx, obj, old) + } + return config +} + // TransformFunc is a function to transform and return newObj type TransformFunc func(ctx context.Context, newObj runtime.Object, oldObj runtime.Object) (transformedNewObj runtime.Object, err error) diff --git a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go index f0e355e54f920..5af704ef57195 100644 --- a/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go +++ b/staging/src/k8s.io/apiserver/pkg/registry/rest/validate.go @@ -34,57 +34,63 @@ import ( "k8s.io/klog/v2" ) -// ValidationConfig defines how a declarative validation request may be configured. -type ValidationConfig func(*validationConfigOption) - -// WithOptions sets the validation options. -// Options should contain any validation options that the declarative validation -// tags expect. These often correspond to feature gates. -func WithOptions(options []string) ValidationConfig { - return func(config *validationConfigOption) { - config.options = options - } +// DeclarativeValidationStrategy defines how a strategy may opt-in to declarative validation. +// +// When strategies implements ValidateDeclaratively and handwritten validation (Validate / ValidateUpdate), +// the errors of both are merged and migration checks are performed. +type DeclarativeValidationStrategy interface { + // ValidateDeclaratively runs declarative validation, merges the declarative validation errors with any + // validationErrs returned from the strategy's Validate / ValidateUpdate functions (which implement hand-written validation) + // and performs migration checks. + ValidateDeclaratively(ctx context.Context, obj, oldObj runtime.Object, validationErrs field.ErrorList, opType operation.Type, config DeclarativeValidationConfig) field.ErrorList } -// WithSubresourceMapper sets the subresource mapper for validation. -// This should be used when registering validation for polymorphic subresources like /scale. -// -// For example, the deployments/scale subresource mapper might map from: +// DeclarativeValidation is an implementation of DeclarativeValidationStrategy that +// provides a convenient way for a strategy to opt-in to declarative validation. // -// group: apps, version: v1, subresource=scale +// For example: // -// to a target of: +// type podStrategy struct { +// rest.DeclarativeValidation +// names.NameGenerator +// } +// var Strategy = podStrategy{rest.DeclarativeValidation{Scheme: legacyscheme.Scheme}, names.SimpleNameGenerator} // -// group: autoscaling, version: v1, kind=Scale -// -// When set, the group version in the requestInfo of the ctx provided to a declarative validation -// request will be passed to the subresource mapper to find the group version kind of the subresource. -// Declarative validation will then convert the object to the subresource group version kind and validate it. -// -// Note that the target of the mapping contains no subresource part since the mapper is expected to -// map to the group version kind of the subresource. -func WithSubresourceMapper(subresourceMapper GroupVersionKindProvider) ValidationConfig { - return func(config *validationConfigOption) { - config.subresourceGVKMapper = subresourceMapper - } +// Once a strategy opts-in this way, any generated declarative validation code is run automatically. +type DeclarativeValidation struct { + *runtime.Scheme } -// WithNormalizationRules sets the normalization rules for validation. -func WithNormalizationRules(rules []field.NormalizationRule) ValidationConfig { - return func(config *validationConfigOption) { - config.normalizationRules = rules - } +func (d DeclarativeValidation) ValidateDeclaratively(ctx context.Context, obj, oldObj runtime.Object, validationErrs field.ErrorList, opType operation.Type, config DeclarativeValidationConfig) field.ErrorList { + return ValidateDeclarativelyWithMigrationChecks(ctx, d.Scheme, obj, oldObj, validationErrs, opType, config) } -// WithDeclarativeEnforcement marks the validation configuration to indicate that it includes -// declarative validations that should follow the fine-grained Validation Lifecycle. -// When set, declarative validation is always executed regardless of feature gates. -// Authority is determined by individual tag prefixes (+k8s:alpha, +k8s:beta) and the -// DeclarativeValidationBeta safety switch. -func WithDeclarativeEnforcement() ValidationConfig { - return func(config *validationConfigOption) { - config.declarativeEnforcement = true - } +// DeclarativeValidationConfigurer defines how a strategy may opt-in to configuration of declarative validation. +type DeclarativeValidationConfigurer interface { + // DeclarativeValidationConfig configures declarative validation for a single request. + DeclarativeValidationConfig(ctx context.Context, obj, oldObj runtime.Object) DeclarativeValidationConfig +} + +// DeclarativeValidationConfig holds configuration for declarative validation. +// Strategies that need to customize declarative validation behavior implement +// DeclarativeValidationConfigurer and return this struct. +type DeclarativeValidationConfig struct { + // Options contains validation options that declarative validation tags + // expect. These often correspond to feature gates. + Options []string + + // DeclarativeEnforcement indicates that declarative validations should + // follow the fine-grained Validation Lifecycle. When set, declarative + // validation is always executed regardless of feature gates. + DeclarativeEnforcement bool + + // NormalizationRules are applied to field paths when comparing + // handwritten and declarative validation errors. + NormalizationRules []field.NormalizationRule + + // SubresourceGVKMapper maps a subresource request to the GVK of the + // subresource type for polymorphic subresources like /scale. + SubresourceGVKMapper GroupVersionKindProvider } type allDeclarativeEnforcedKeyType struct{} @@ -100,13 +106,12 @@ func WithAllDeclarativeEnforcedForTest(ctx context.Context) context.Context { return context.WithValue(ctx, allDeclarativeEnforcedKey, true) } -type validationConfigOption struct { - opType operation.Type - options []string - subresourceGVKMapper GroupVersionKindProvider - validationIdentifier string - normalizationRules []field.NormalizationRule - declarativeEnforcement bool +// ValidationConfigOption is the internal configuration used by +// ValidateDeclarativelyWithMigrationChecks. It is exported for use in tests. +type ValidationConfigOption struct { + OpType operation.Type + ValidationIdentifier string + DeclarativeValidationConfig } // validateDeclaratively validates obj and oldObj against declarative @@ -121,9 +126,9 @@ type validationConfigOption struct { // Returns a field.ErrorList containing any validation errors. An internal error // is included if requestInfo is missing from the context or if version // conversion fails. -func validateDeclaratively(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *validationConfigOption) field.ErrorList { +func validateDeclaratively(ctx context.Context, scheme *runtime.Scheme, obj, oldObj runtime.Object, o *ValidationConfigOption) field.ErrorList { // Find versionedGroupVersion, which identifies the API version to use for declarative validation. - ... [truncated]
← Back to Alerts View on GitHub →