Limit YAML/JSON decode size

This commit is contained in:
Jordan Liggitt 2019-09-27 16:36:48 -04:00
parent 8aeffa8716
commit 8ef4566cef
16 changed files with 1063 additions and 47 deletions

View File

@ -130,8 +130,8 @@ func TestAddFlags(t *testing.T) {
MaxMutatingRequestsInFlight: 200,
RequestTimeout: time.Duration(2) * time.Minute,
MinRequestTimeout: 1800,
JSONPatchMaxCopyBytes: int64(100 * 1024 * 1024),
MaxRequestBodyBytes: int64(100 * 1024 * 1024),
JSONPatchMaxCopyBytes: int64(3 * 1024 * 1024),
MaxRequestBodyBytes: int64(3 * 1024 * 1024),
},
Admission: &kubeoptions.AdmissionOptions{
GenericAdmission: &apiserveroptions.AdmissionOptions{

View File

@ -201,6 +201,7 @@ func (c completedConfig) New(delegationTarget genericapiserver.DelegationTarget)
c.GenericConfig.RequestTimeout,
time.Duration(c.GenericConfig.MinRequestTimeout)*time.Second,
apiGroupInfo.StaticOpenAPISpec,
c.GenericConfig.MaxRequestBodyBytes,
)
if err != nil {
return nil, err

View File

@ -125,6 +125,10 @@ type crdHandler struct {
// purpose of managing fields, it is how CR handlers get the structure
// of TypeMeta and ObjectMeta
staticOpenAPISpec *spec.Swagger
// The limit on the request size that would be accepted and decoded in a write request
// 0 means no limit.
maxRequestBodyBytes int64
}
// crdInfo stores enough information to serve the storage for the custom resource
@ -169,7 +173,8 @@ func NewCustomResourceDefinitionHandler(
authorizer authorizer.Authorizer,
requestTimeout time.Duration,
minRequestTimeout time.Duration,
staticOpenAPISpec *spec.Swagger) (*crdHandler, error) {
staticOpenAPISpec *spec.Swagger,
maxRequestBodyBytes int64) (*crdHandler, error) {
ret := &crdHandler{
versionDiscoveryHandler: versionDiscoveryHandler,
groupDiscoveryHandler: groupDiscoveryHandler,
@ -185,6 +190,7 @@ func NewCustomResourceDefinitionHandler(
requestTimeout: requestTimeout,
minRequestTimeout: minRequestTimeout,
staticOpenAPISpec: staticOpenAPISpec,
maxRequestBodyBytes: maxRequestBodyBytes,
}
crdInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
AddFunc: ret.createCustomResourceDefinition,
@ -812,6 +818,8 @@ func (r *crdHandler) getOrCreateServingInfoFor(uid types.UID, name string) (*crd
TableConvertor: storages[v.Name].CustomResource,
Authorizer: r.authorizer,
MaxRequestBodyBytes: r.maxRequestBodyBytes,
}
if utilfeature.DefaultFeatureGate.Enabled(features.ServerSideApply) {
reqScope := *requestScopes[v.Name]

View File

@ -15,6 +15,7 @@ go_test(
"change_test.go",
"defaulting_test.go",
"finalization_test.go",
"limit_test.go",
"objectmeta_test.go",
"pruning_test.go",
"registration_test.go",

View File

@ -0,0 +1,216 @@
/*
Copyright 2019 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 integration
import (
"fmt"
"strings"
"testing"
"k8s.io/client-go/dynamic"
apiextensionsv1beta1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1beta1"
"k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
"k8s.io/apiextensions-apiserver/test/integration/fixtures"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/types"
)
func TestLimits(t *testing.T) {
tearDown, config, _, err := fixtures.StartDefaultServer(t)
if err != nil {
t.Fatal(err)
}
defer tearDown()
apiExtensionClient, err := clientset.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
dynamicClient, err := dynamic.NewForConfig(config)
if err != nil {
t.Fatal(err)
}
noxuDefinition := fixtures.NewNoxuCustomResourceDefinition(apiextensionsv1beta1.ClusterScoped)
noxuDefinition, err = fixtures.CreateNewCustomResourceDefinition(noxuDefinition, apiExtensionClient, dynamicClient)
if err != nil {
t.Fatal(err)
}
kind := noxuDefinition.Spec.Names.Kind
apiVersion := noxuDefinition.Spec.Group + "/" + noxuDefinition.Spec.Version
rest := apiExtensionClient.Discovery().RESTClient()
// Create YAML over 3MB limit
t.Run("create YAML over limit", func(t *testing.T) {
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: test
values: `+strings.Repeat("[", 3*1024*1024), apiVersion, kind))
_, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if !apierrors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected too large error, got %v", err)
}
})
// Create YAML just under 3MB limit, nested
t.Run("create YAML doc under limit, nested", func(t *testing.T) {
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: test
values: `+strings.Repeat("[", 3*1024*1024/2-500)+strings.Repeat("]", 3*1024*1024/2-500), apiVersion, kind))
_, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create YAML just under 3MB limit, not nested
t.Run("create YAML doc under limit, not nested", func(t *testing.T) {
yamlBody := []byte(fmt.Sprintf(`
apiVersion: %s
kind: %s
metadata:
name: test
values: `+strings.Repeat("[", 3*1024*1024-1000), apiVersion, kind))
_, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(yamlBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create JSON over 3MB limit
t.Run("create JSON over limit", func(t *testing.T) {
jsonBody := []byte(fmt.Sprintf(`{
"apiVersion": %q,
"kind": %q,
"metadata": {
"name": "test"
},
"values": `+strings.Repeat("[", 3*1024*1024/2)+strings.Repeat("]", 3*1024*1024/2)+"}", apiVersion, kind))
_, err := rest.Post().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(jsonBody).
DoRaw()
if !apierrors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected too large error, got %v", err)
}
})
// Create JSON just under 3MB limit, nested
t.Run("create JSON doc under limit, nested", func(t *testing.T) {
jsonBody := []byte(fmt.Sprintf(`{
"apiVersion": %q,
"kind": %q,
"metadata": {
"name": "test"
},
"values": `+strings.Repeat("[", 3*1024*1024/2-500)+strings.Repeat("]", 3*1024*1024/2-500)+"}", apiVersion, kind))
_, err := rest.Post().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(jsonBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create JSON just under 3MB limit, not nested
t.Run("create JSON doc under limit, not nested", func(t *testing.T) {
jsonBody := []byte(fmt.Sprintf(`{
"apiVersion": %q,
"kind": %q,
"metadata": {
"name": "test"
},
"values": `+strings.Repeat("[", 3*1024*1024-1000)+"}", apiVersion, kind))
_, err := rest.Post().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).
Body(jsonBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create instance to allow patching
{
jsonBody := []byte(fmt.Sprintf(`{"apiVersion": %q, "kind": %q, "metadata": {"name": "test"}}`, apiVersion, kind))
_, err := rest.Post().AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural).Body(jsonBody).DoRaw()
if err != nil {
t.Fatalf("error creating object: %v", err)
}
}
t.Run("JSONPatchType nested patch under limit", func(t *testing.T) {
patchBody := []byte(`[{"op":"add","path":"/foo","value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}]`)
err = rest.Patch(types.JSONPatchType).AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "test").
Body(patchBody).Do().Error()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected success or bad request err, got %v", err)
}
})
t.Run("MergePatchType nested patch under limit", func(t *testing.T) {
patchBody := []byte(`{"value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}`)
err = rest.Patch(types.MergePatchType).AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "test").
Body(patchBody).Do().Error()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected success or bad request err, got %v", err)
}
})
t.Run("ApplyPatchType nested patch under limit", func(t *testing.T) {
patchBody := []byte(`{"value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}`)
err = rest.Patch(types.ApplyPatchType).Param("fieldManager", "test").AbsPath("/apis", noxuDefinition.Spec.Group, noxuDefinition.Spec.Version, noxuDefinition.Spec.Names.Plural, "test").
Body(patchBody).Do().Error()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request err, got %#v", err)
}
})
}

View File

@ -9,6 +9,7 @@ load(
go_test(
name = "go_default_test",
srcs = [
"json_limit_test.go",
"json_test.go",
"meta_test.go",
],
@ -17,6 +18,7 @@ go_test(
"//staging/src/k8s.io/apimachinery/pkg/runtime:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/runtime/schema:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library",
"//staging/src/k8s.io/apimachinery/pkg/util/json:go_default_library",
],
)

View File

@ -122,7 +122,27 @@ func (customNumberDecoder) Decode(ptr unsafe.Pointer, iter *jsoniter.Iterator) {
}
iter.ReportError("DecodeNumber", err.Error())
default:
// init depth, if needed
if iter.Attachment == nil {
iter.Attachment = int(1)
}
// remember current depth
originalAttachment := iter.Attachment
// increment depth before descending
if i, ok := iter.Attachment.(int); ok {
iter.Attachment = i + 1
if i > 10000 {
iter.ReportError("parse", "exceeded max depth")
return
}
}
*(*interface{})(ptr) = iter.Read()
// restore current depth
iter.Attachment = originalAttachment
}
}

View File

@ -0,0 +1,170 @@
/*
Copyright 2019 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 json
import (
gojson "encoding/json"
"strings"
"testing"
utiljson "k8s.io/apimachinery/pkg/util/json"
)
type testcase struct {
name string
data []byte
checkErr func(t testing.TB, err error)
benchmark bool
}
func testcases() []testcase {
// verify we got an error of some kind
nonNilError := func(t testing.TB, err error) {
if err == nil {
t.Errorf("expected error, got none")
}
}
// verify the parse completed, either with success or a max depth error
successOrMaxDepthError := func(t testing.TB, err error) {
if err != nil && !strings.Contains(err.Error(), "max depth") {
t.Errorf("expected success or error containing 'max depth', got: %v", err)
}
}
return []testcase{
{
name: "3MB of deeply nested slices",
checkErr: successOrMaxDepthError,
data: []byte(`{"a":` + strings.Repeat(`[`, 3*1024*1024/2) + strings.Repeat(`]`, 3*1024*1024/2) + "}"),
},
{
name: "3MB of unbalanced nested slices",
checkErr: nonNilError,
data: []byte(`{"a":` + strings.Repeat(`[`, 3*1024*1024)),
},
{
name: "3MB of deeply nested maps",
checkErr: successOrMaxDepthError,
data: []byte(strings.Repeat(`{"":`, 3*1024*1024/5/2) + "{}" + strings.Repeat(`}`, 3*1024*1024/5/2)),
},
{
name: "3MB of unbalanced nested maps",
checkErr: nonNilError,
data: []byte(strings.Repeat(`{"":`, 3*1024*1024/5)),
},
{
name: "3MB of empty slices",
data: []byte(`{"a":[` + strings.Repeat(`[],`, 3*1024*1024/3-2) + `[]]}`),
benchmark: true,
},
{
name: "3MB of slices",
data: []byte(`{"a":[` + strings.Repeat(`[0],`, 3*1024*1024/4-2) + `[0]]}`),
benchmark: true,
},
{
name: "3MB of empty maps",
data: []byte(`{"a":[` + strings.Repeat(`{},`, 3*1024*1024/3-2) + `{}]}`),
benchmark: true,
},
{
name: "3MB of maps",
data: []byte(`{"a":[` + strings.Repeat(`{"a":0},`, 3*1024*1024/8-2) + `{"a":0}]}`),
benchmark: true,
},
{
name: "3MB of ints",
data: []byte(`{"a":[` + strings.Repeat(`0,`, 3*1024*1024/2-2) + `0]}`),
benchmark: true,
},
{
name: "3MB of floats",
data: []byte(`{"a":[` + strings.Repeat(`0.0,`, 3*1024*1024/4-2) + `0.0]}`),
benchmark: true,
},
{
name: "3MB of bools",
data: []byte(`{"a":[` + strings.Repeat(`true,`, 3*1024*1024/5-2) + `true]}`),
benchmark: true,
},
{
name: "3MB of empty strings",
data: []byte(`{"a":[` + strings.Repeat(`"",`, 3*1024*1024/3-2) + `""]}`),
benchmark: true,
},
{
name: "3MB of strings",
data: []byte(`{"a":[` + strings.Repeat(`"abcdefghijklmnopqrstuvwxyz012",`, 3*1024*1024/30-2) + `"abcdefghijklmnopqrstuvwxyz012"]}`),
benchmark: true,
},
{
name: "3MB of nulls",
data: []byte(`{"a":[` + strings.Repeat(`null,`, 3*1024*1024/5-2) + `null]}`),
benchmark: true,
},
}
}
var decoders = map[string]func([]byte, interface{}) error{
"gojson": gojson.Unmarshal,
"utiljson": utiljson.Unmarshal,
"jsoniter": CaseSensitiveJsonIterator().Unmarshal,
}
func TestJSONLimits(t *testing.T) {
for _, tc := range testcases() {
if tc.benchmark {
continue
}
t.Run(tc.name, func(t *testing.T) {
for decoderName, decoder := range decoders {
t.Run(decoderName, func(t *testing.T) {
v := map[string]interface{}{}
err := decoder(tc.data, &v)
if tc.checkErr != nil {
tc.checkErr(t, err)
} else if err != nil {
t.Errorf("unexpected error: %v", err)
}
})
}
})
}
}
func BenchmarkJSONLimits(b *testing.B) {
for _, tc := range testcases() {
b.Run(tc.name, func(b *testing.B) {
for decoderName, decoder := range decoders {
b.Run(decoderName, func(b *testing.B) {
for i := 0; i < b.N; i++ {
v := map[string]interface{}{}
err := decoder(tc.data, &v)
if tc.checkErr != nil {
tc.checkErr(b, err)
} else if err != nil {
b.Errorf("unexpected error: %v", err)
}
}
})
}
})
}
}

View File

@ -1,9 +1,6 @@
package(default_visibility = ["//visibility:public"])
load(
"@io_bazel_rules_go//go:def.bzl",
"go_library",
)
load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
@ -29,3 +26,14 @@ filegroup(
srcs = [":package-srcs"],
tags = ["automanaged"],
)
go_test(
name = "go_default_test",
srcs = ["yaml_test.go"],
data = glob(["testdata/**"]),
embed = [":go_default_library"],
deps = [
"//staging/src/k8s.io/apimachinery/pkg/util/yaml:go_default_library",
"//vendor/sigs.k8s.io/yaml:go_default_library",
],
)

View File

@ -0,0 +1,402 @@
/*
Copyright 2019 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 yaml
import (
"strings"
"testing"
"k8s.io/apimachinery/pkg/util/yaml"
sigsyaml "sigs.k8s.io/yaml"
)
type testcase struct {
name string
data []byte
error string
benchmark bool
}
func testcases() []testcase {
return []testcase{
{
name: "arrays of string aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a ["webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb","webwebwebwebwebweb"]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "arrays of empty string aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a ["","","","","","","","",""]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "arrays of null aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a [null,null,null,null,null,null,null,null,null]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "arrays of zero int aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a [0,0,0,0,0,0,0,0,0]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "arrays of zero float aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a [0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0,0.0]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "arrays of big float aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a [1234567890.12345678,1234567890.12345678,1234567890.12345678,1234567890.12345678,1234567890.12345678,1234567890.12345678,1234567890.12345678,1234567890.12345678,1234567890.12345678]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "arrays of bool aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a [true,true,true,true,true,true,true,true,true]
b: &b [*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a,*a]
c: &c [*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b,*b]
d: &d [*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c,*c]
e: &e [*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d,*d]
f: &f [*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e,*e]
g: &g [*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f,*f]
h: &h [*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g,*g]
i: &i [*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h,*h]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "map key aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a {"verylongkey1":"","verylongkey2":"","verylongkey3":"","verylongkey4":"","verylongkey5":"","verylongkey6":"","verylongkey7":"","verylongkey8":"","verylongkey9":""}
b: &b {"verylongkey1":*a,"verylongkey2":*a,"verylongkey3":*a,"verylongkey4":*a,"verylongkey5":*a,"verylongkey6":*a,"verylongkey7":*a,"verylongkey8":*a,"verylongkey9":*a}
c: &c {"verylongkey1":*b,"verylongkey2":*b,"verylongkey3":*b,"verylongkey4":*b,"verylongkey5":*b,"verylongkey6":*b,"verylongkey7":*b,"verylongkey8":*b,"verylongkey9":*b}
d: &d {"verylongkey1":*c,"verylongkey2":*c,"verylongkey3":*c,"verylongkey4":*c,"verylongkey5":*c,"verylongkey6":*c,"verylongkey7":*c,"verylongkey8":*c,"verylongkey9":*c}
e: &e {"verylongkey1":*d,"verylongkey2":*d,"verylongkey3":*d,"verylongkey4":*d,"verylongkey5":*d,"verylongkey6":*d,"verylongkey7":*d,"verylongkey8":*d,"verylongkey9":*d}
f: &f {"verylongkey1":*e,"verylongkey2":*e,"verylongkey3":*e,"verylongkey4":*e,"verylongkey5":*e,"verylongkey6":*e,"verylongkey7":*e,"verylongkey8":*e,"verylongkey9":*e}
g: &g {"verylongkey1":*f,"verylongkey2":*f,"verylongkey3":*f,"verylongkey4":*f,"verylongkey5":*f,"verylongkey6":*f,"verylongkey7":*f,"verylongkey8":*f,"verylongkey9":*f}
h: &h {"verylongkey1":*g,"verylongkey2":*g,"verylongkey3":*g,"verylongkey4":*g,"verylongkey5":*g,"verylongkey6":*g,"verylongkey7":*g,"verylongkey8":*g,"verylongkey9":*g}
i: &i {"verylongkey1":*h,"verylongkey2":*h,"verylongkey3":*h,"verylongkey4":*h,"verylongkey5":*h,"verylongkey6":*h,"verylongkey7":*h,"verylongkey8":*h,"verylongkey9":*h}
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "map value aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a {"1":"verylongmapvalue","2":"verylongmapvalue","3":"verylongmapvalue","4":"verylongmapvalue","5":"verylongmapvalue","6":"verylongmapvalue","7":"verylongmapvalue","8":"verylongmapvalue","9":"verylongmapvalue"}
b: &b {"1":*a,"2":*a,"3":*a,"4":*a,"5":*a,"6":*a,"7":*a,"8":*a,"9":*a}
c: &c {"1":*b,"2":*b,"3":*b,"4":*b,"5":*b,"6":*b,"7":*b,"8":*b,"9":*b}
d: &d {"1":*c,"2":*c,"3":*c,"4":*c,"5":*c,"6":*c,"7":*c,"8":*c,"9":*c}
e: &e {"1":*d,"2":*d,"3":*d,"4":*d,"5":*d,"6":*d,"7":*d,"8":*d,"9":*d}
f: &f {"1":*e,"2":*e,"3":*e,"4":*e,"5":*e,"6":*e,"7":*e,"8":*e,"9":*e}
g: &g {"1":*f,"2":*f,"3":*f,"4":*f,"5":*f,"6":*f,"7":*f,"8":*f,"9":*f}
h: &h {"1":*g,"2":*g,"3":*g,"4":*g,"5":*g,"6":*g,"7":*g,"8":*g,"9":*g}
i: &i {"1":*h,"2":*h,"3":*h,"4":*h,"5":*h,"6":*h,"7":*h,"8":*h,"9":*h}
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "nested map aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a {"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{"":{}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}}
b: &b {"1":*a,"2":*a,"3":*a,"4":*a,"5":*a,"6":*a,"7":*a,"8":*a,"9":*a}
c: &c {"1":*b,"2":*b,"3":*b,"4":*b,"5":*b,"6":*b,"7":*b,"8":*b,"9":*b}
d: &d {"1":*c,"2":*c,"3":*c,"4":*c,"5":*c,"6":*c,"7":*c,"8":*c,"9":*c}
e: &e {"1":*d,"2":*d,"3":*d,"4":*d,"5":*d,"6":*d,"7":*d,"8":*d,"9":*d}
f: &f {"1":*e,"2":*e,"3":*e,"4":*e,"5":*e,"6":*e,"7":*e,"8":*e,"9":*e}
g: &g {"1":*f,"2":*f,"3":*f,"4":*f,"5":*f,"6":*f,"7":*f,"8":*f,"9":*f}
h: &h {"1":*g,"2":*g,"3":*g,"4":*g,"5":*g,"6":*g,"7":*g,"8":*g,"9":*g}
i: &i {"1":*h,"2":*h,"3":*h,"4":*h,"5":*h,"6":*h,"7":*h,"8":*h,"9":*h}
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "nested slice aliases",
error: "excessive aliasing",
data: []byte(`
apiVersion: v1
data:
a: &a [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[""]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]
b: &b [[[[[[[[[[*a]]]]]]]]],[[[[[[[[[*a]]]]]]]]],[[[[[[[[[*a]]]]]]]]],[[[[[[[[[*a]]]]]]]]],[[[[[[[[[*a]]]]]]]]]]
c: &c [[[[[[[[[[*b]]]]]]]]],[[[[[[[[[*b]]]]]]]]],[[[[[[[[[*b]]]]]]]]],[[[[[[[[[*b]]]]]]]]],[[[[[[[[[*b]]]]]]]]]]
d: &d [[[[[[[[[[*c]]]]]]]]],[[[[[[[[[*c]]]]]]]]],[[[[[[[[[*c]]]]]]]]],[[[[[[[[[*c]]]]]]]]],[[[[[[[[[*c]]]]]]]]]]
e: &e [[[[[[[[[[*d]]]]]]]]],[[[[[[[[[*d]]]]]]]]],[[[[[[[[[*d]]]]]]]]],[[[[[[[[[*d]]]]]]]]],[[[[[[[[[*d]]]]]]]]]]
f: &f [[[[[[[[[[*e]]]]]]]]],[[[[[[[[[*e]]]]]]]]],[[[[[[[[[*e]]]]]]]]],[[[[[[[[[*e]]]]]]]]],[[[[[[[[[*e]]]]]]]]]]
g: &g [[[[[[[[[[*f]]]]]]]]],[[[[[[[[[*f]]]]]]]]],[[[[[[[[[*f]]]]]]]]],[[[[[[[[[*f]]]]]]]]],[[[[[[[[[*f]]]]]]]]]]
h: &h [[[[[[[[[[*g]]]]]]]]],[[[[[[[[[*g]]]]]]]]],[[[[[[[[[*g]]]]]]]]],[[[[[[[[[*g]]]]]]]]],[[[[[[[[[*g]]]]]]]]]]
i: &i [[[[[[[[[[*h]]]]]]]]],[[[[[[[[[*h]]]]]]]]],[[[[[[[[[*h]]]]]]]]],[[[[[[[[[*h]]]]]]]]],[[[[[[[[[*h]]]]]]]]]]
kind: ConfigMap
metadata:
name: yaml-bomb
namespace: default
`),
},
{
name: "3MB map without alias",
data: []byte(`a: &a [{a}` + strings.Repeat(`,{a}`, 3*1024*1024/4) + `]`),
},
{
name: "3MB map with alias",
error: "excessive aliasing",
data: []byte(`
a: &a [{a}` + strings.Repeat(`,{a}`, 3*1024*1024/4) + `]
b: &b [*a]`),
},
{
name: "deeply nested slices",
error: "max depth",
data: []byte(strings.Repeat(`[`, 3*1024*1024)),
},
{
name: "deeply nested maps",
error: "max depth",
data: []byte("x: " + strings.Repeat(`{`, 3*1024*1024)),
},
{
name: "deeply nested indents",
error: "max depth",
data: []byte(strings.Repeat(`- `, 3*1024*1024)),
},
{
name: "3MB of 1000-indent lines",
data: []byte(strings.Repeat(strings.Repeat(`- `, 1000)+"\n", 3*1024/2)),
benchmark: true,
},
{
name: "3MB of empty slices",
data: []byte(`[` + strings.Repeat(`[],`, 3*1024*1024/3-2) + `[]]`),
benchmark: true,
},
{
name: "3MB of slices",
data: []byte(`[` + strings.Repeat(`[0],`, 3*1024*1024/4-2) + `[0]]`),
benchmark: true,
},
{
name: "3MB of empty maps",
data: []byte(`[` + strings.Repeat(`{},`, 3*1024*1024/3-2) + `{}]`),
benchmark: true,
},
{
name: "3MB of maps",
data: []byte(`[` + strings.Repeat(`{a},`, 3*1024*1024/4-2) + `{a}]`),
benchmark: true,
},
{
name: "3MB of ints",
data: []byte(`[` + strings.Repeat(`0,`, 3*1024*1024/2-2) + `0]`),
benchmark: true,
},
{
name: "3MB of floats",
data: []byte(`[` + strings.Repeat(`0.0,`, 3*1024*1024/4-2) + `0.0]`),
benchmark: true,
},
{
name: "3MB of bools",
data: []byte(`[` + strings.Repeat(`true,`, 3*1024*1024/5-2) + `true]`),
benchmark: true,
},
{
name: "3MB of empty strings",
data: []byte(`[` + strings.Repeat(`"",`, 3*1024*1024/3-2) + `""]`),
benchmark: true,
},
{
name: "3MB of strings",
data: []byte(`[` + strings.Repeat(`"abcdefghijklmnopqrstuvwxyz012",`, 3*1024*1024/30-2) + `"abcdefghijklmnopqrstuvwxyz012"]`),
benchmark: true,
},
{
name: "3MB of nulls",
data: []byte(`[` + strings.Repeat(`null,`, 3*1024*1024/5-2) + `null]`),
benchmark: true,
},
}
}
var decoders = map[string]func([]byte) ([]byte, error){
"sigsyaml": sigsyaml.YAMLToJSON,
"utilyaml": yaml.ToJSON,
}
func TestYAMLLimits(t *testing.T) {
for _, tc := range testcases() {
if tc.benchmark {
continue
}
t.Run(tc.name, func(t *testing.T) {
for decoderName, decoder := range decoders {
t.Run(decoderName, func(t *testing.T) {
_, err := decoder(tc.data)
if len(tc.error) == 0 {
if err != nil {
t.Errorf("unexpected error: %v", err)
}
} else {
if err == nil || !strings.Contains(err.Error(), tc.error) {
t.Errorf("expected %q error, got %v", tc.error, err)
}
}
})
}
})
}
}
func BenchmarkYAMLLimits(b *testing.B) {
for _, tc := range testcases() {
b.Run(tc.name, func(b *testing.B) {
for decoderName, decoder := range decoders {
b.Run(decoderName, func(b *testing.B) {
for i := 0; i < b.N; i++ {
_, err := decoder(tc.data)
if len(tc.error) == 0 {
if err != nil {
b.Errorf("unexpected error: %v", err)
}
} else {
if err == nil || !strings.Contains(err.Error(), tc.error) {
b.Errorf("expected %q error, got %v", tc.error, err)
}
}
}
})
}
})
}
}

View File

@ -19,6 +19,7 @@ package json
import (
"bytes"
"encoding/json"
"fmt"
"io"
)
@ -34,6 +35,9 @@ func Marshal(v interface{}) ([]byte, error) {
return json.Marshal(v)
}
// limit recursive depth to prevent stack overflow errors
const maxDepth = 10000
// Unmarshal unmarshals the given data
// If v is a *map[string]interface{}, numbers are converted to int64 or float64
func Unmarshal(data []byte, v interface{}) error {
@ -48,7 +52,7 @@ func Unmarshal(data []byte, v interface{}) error {
return err
}
// If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64
return convertMapNumbers(*v)
return convertMapNumbers(*v, 0)
case *[]interface{}:
// Build a decoder from the given data
@ -60,7 +64,7 @@ func Unmarshal(data []byte, v interface{}) error {
return err
}
// If the decode succeeds, post-process the map to convert json.Number objects to int64 or float64
return convertSliceNumbers(*v)
return convertSliceNumbers(*v, 0)
default:
return json.Unmarshal(data, v)
@ -69,16 +73,20 @@ func Unmarshal(data []byte, v interface{}) error {
// convertMapNumbers traverses the map, converting any json.Number values to int64 or float64.
// values which are map[string]interface{} or []interface{} are recursively visited
func convertMapNumbers(m map[string]interface{}) error {
func convertMapNumbers(m map[string]interface{}, depth int) error {
if depth > maxDepth {
return fmt.Errorf("exceeded max depth of %d", maxDepth)
}
var err error
for k, v := range m {
switch v := v.(type) {
case json.Number:
m[k], err = convertNumber(v)
case map[string]interface{}:
err = convertMapNumbers(v)
err = convertMapNumbers(v, depth+1)
case []interface{}:
err = convertSliceNumbers(v)
err = convertSliceNumbers(v, depth+1)
}
if err != nil {
return err
@ -89,16 +97,20 @@ func convertMapNumbers(m map[string]interface{}) error {
// convertSliceNumbers traverses the slice, converting any json.Number values to int64 or float64.
// values which are map[string]interface{} or []interface{} are recursively visited
func convertSliceNumbers(s []interface{}) error {
func convertSliceNumbers(s []interface{}, depth int) error {
if depth > maxDepth {
return fmt.Errorf("exceeded max depth of %d", maxDepth)
}
var err error
for i, v := range s {
switch v := v.(type) {
case json.Number:
s[i], err = convertNumber(v)
case map[string]interface{}:
err = convertMapNumbers(v)
err = convertMapNumbers(v, depth+1)
case []interface{}:
err = convertSliceNumbers(v)
err = convertSliceNumbers(v, depth+1)
}
if err != nil {
return err

View File

@ -195,11 +195,11 @@ func (f *fieldManager) Apply(liveObj runtime.Object, patch []byte, fieldManager
patchObj := &unstructured.Unstructured{Object: map[string]interface{}{}}
if err := yaml.Unmarshal(patch, &patchObj.Object); err != nil {
return nil, fmt.Errorf("error decoding YAML: %v", err)
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding YAML: %v", err))
}
if patchObj.GetManagedFields() != nil {
return nil, fmt.Errorf("managed fields must be nil but was %v", patchObj.GetManagedFields())
return nil, errors.NewBadRequest(fmt.Sprintf("metadata.managedFields must be nil"))
}
if patchObj.GetAPIVersion() != f.groupVersion.String() {

View File

@ -337,6 +337,15 @@ func (p *jsonPatcher) createNewObject() (runtime.Object, error) {
func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr error) {
switch p.patchType {
case types.JSONPatchType:
// sanity check potentially abusive patches
// TODO(liggitt): drop this once golang json parser limits stack depth (https://github.com/golang/go/issues/31789)
if len(p.patchBytes) > 1024*1024 {
v := []interface{}{}
if err := json.Unmarshal(p.patchBytes, v); err != nil {
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding patch: %v", err))
}
}
patchObj, err := jsonpatch.DecodePatch(p.patchBytes)
if err != nil {
return nil, errors.NewBadRequest(err.Error())
@ -352,6 +361,15 @@ func (p *jsonPatcher) applyJSPatch(versionedJS []byte) (patchedJS []byte, retErr
}
return patchedJS, nil
case types.MergePatchType:
// sanity check potentially abusive patches
// TODO(liggitt): drop this once golang json parser limits stack depth (https://github.com/golang/go/issues/31789)
if len(p.patchBytes) > 1024*1024 {
v := map[string]interface{}{}
if err := json.Unmarshal(p.patchBytes, v); err != nil {
return nil, errors.NewBadRequest(fmt.Sprintf("error decoding patch: %v", err))
}
}
return jsonpatch.MergePatch(versionedJS, p.patchBytes)
default:
// only here as a safety net - go-restful filters content-type

View File

@ -180,7 +180,7 @@ type Config struct {
// patch may cause.
// This affects all places that applies json patch in the binary.
JSONPatchMaxCopyBytes int64
// The limit on the request body size that would be accepted and decoded in a write request.
// The limit on the request size that would be accepted and decoded in a write request
// 0 means no limit.
MaxRequestBodyBytes int64
// MaxRequestsInFlight is the maximum number of parallel non-long-running requests. Every further
@ -295,22 +295,20 @@ func NewConfig(codecs serializer.CodecFactory) *Config {
MinRequestTimeout: 1800,
LivezGracePeriod: time.Duration(0),
ShutdownDelayDuration: time.Duration(0),
// 10MB is the recommended maximum client request size in bytes
// 1.5MB is the default client request size in bytes
// the etcd server should accept. See
// https://github.com/etcd-io/etcd/blob/release-3.3/etcdserver/server.go#L90.
// https://github.com/etcd-io/etcd/blob/release-3.4/embed/config.go#L56.
// A request body might be encoded in json, and is converted to
// proto when persisted in etcd. Assuming the upper bound of
// the size ratio is 10:1, we set 100MB as the largest size
// proto when persisted in etcd, so we allow 2x as the largest size
// increase the "copy" operations in a json patch may cause.
JSONPatchMaxCopyBytes: int64(100 * 1024 * 1024),
// 10MB is the recommended maximum client request size in bytes
JSONPatchMaxCopyBytes: int64(3 * 1024 * 1024),
// 1.5MB is the recommended client request size in byte
// the etcd server should accept. See
// https://github.com/etcd-io/etcd/blob/release-3.3/etcdserver/server.go#L90.
// https://github.com/etcd-io/etcd/blob/release-3.4/embed/config.go#L56.
// A request body might be encoded in json, and is converted to
// proto when persisted in etcd. Assuming the upper bound of
// the size ratio is 10:1, we set 100MB as the largest request
// proto when persisted in etcd, so we allow 2x as the largest request
// body size to be accepted and decoded in a write request.
MaxRequestBodyBytes: int64(100 * 1024 * 1024),
MaxRequestBodyBytes: int64(3 * 1024 * 1024),
// Default to treating watch as a long-running operation
// Generic API servers have no inherent long-running subresources

View File

@ -21,11 +21,11 @@ import (
"strings"
"testing"
"k8s.io/api/core/v1"
v1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/api/errors"
apierrors "k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
"k8s.io/kubernetes/cmd/kube-apiserver/app/options"
"k8s.io/kubernetes/test/integration/framework"
)
@ -33,13 +33,11 @@ import (
func TestMaxResourceSize(t *testing.T) {
stopCh := make(chan struct{})
defer close(stopCh)
clientSet, _ := framework.StartTestServer(t, stopCh, framework.TestServerSetup{
ModifyServerRunOptions: func(opts *options.ServerRunOptions) {
opts.GenericServerRunOptions.MaxRequestBodyBytes = 1024 * 1024
},
})
clientSet, _ := framework.StartTestServer(t, stopCh, framework.TestServerSetup{})
hugeData := []byte(strings.Repeat("x", 1024*1024+1))
hugeData := []byte(strings.Repeat("x", 3*1024*1024+1))
rest := clientSet.Discovery().RESTClient()
c := clientSet.CoreV1().RESTClient()
t.Run("Create should limit the request body size", func(t *testing.T) {
@ -87,6 +85,38 @@ func TestMaxResourceSize(t *testing.T) {
}
})
t.Run("JSONPatchType should handle a patch just under the max limit", func(t *testing.T) {
patchBody := []byte(`[{"op":"add","path":"/foo","value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}]`)
err = rest.Patch(types.JSONPatchType).AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(patchBody).Do().Error()
if err != nil && !errors.IsBadRequest(err) {
t.Errorf("expected success or bad request err, got %v", err)
}
})
t.Run("MergePatchType should handle a patch just under the max limit", func(t *testing.T) {
patchBody := []byte(`{"value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}`)
err = rest.Patch(types.MergePatchType).AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(patchBody).Do().Error()
if err != nil && !errors.IsBadRequest(err) {
t.Errorf("expected success or bad request err, got %v", err)
}
})
t.Run("StrategicMergePatchType should handle a patch just under the max limit", func(t *testing.T) {
patchBody := []byte(`{"value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}`)
err = rest.Patch(types.StrategicMergePatchType).AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(patchBody).Do().Error()
if err != nil && !errors.IsBadRequest(err) {
t.Errorf("expected success or bad request err, got %v", err)
}
})
t.Run("ApplyPatchType should handle a patch just under the max limit", func(t *testing.T) {
patchBody := []byte(`{"value":` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + `}`)
err = rest.Patch(types.ApplyPatchType).Param("fieldManager", "test").AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(patchBody).Do().Error()
if err != nil && !errors.IsBadRequest(err) {
t.Errorf("expected success or bad request err, got %#v", err)
}
})
t.Run("Delete should limit the request body size", func(t *testing.T) {
err = c.Delete().AbsPath(fmt.Sprintf("/api/v1/namespaces/default/secrets/test")).
Body(hugeData).Do().Error()
@ -98,4 +128,128 @@ func TestMaxResourceSize(t *testing.T) {
}
})
// Create YAML over 3MB limit
t.Run("create should limit yaml parsing", func(t *testing.T) {
yamlBody := []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: mytest
values: ` + strings.Repeat("[", 3*1024*1024))
_, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/api/v1/namespaces/default/configmaps").
Body(yamlBody).
DoRaw()
if !apierrors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected too large error, got %v", err)
}
})
// Create YAML just under 3MB limit, nested
t.Run("create should handle a yaml document just under the maximum size with correct nesting", func(t *testing.T) {
yamlBody := []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: mytest
values: ` + strings.Repeat("[", 3*1024*1024/2-500) + strings.Repeat("]", 3*1024*1024/2-500))
_, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/api/v1/namespaces/default/configmaps").
Body(yamlBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create YAML just under 3MB limit, not nested
t.Run("create should handle a yaml document just under the maximum size with unbalanced nesting", func(t *testing.T) {
yamlBody := []byte(`
apiVersion: v1
kind: ConfigMap
metadata:
name: mytest
values: ` + strings.Repeat("[", 3*1024*1024-1000))
_, err := rest.Post().
SetHeader("Accept", "application/yaml").
SetHeader("Content-Type", "application/yaml").
AbsPath("/api/v1/namespaces/default/configmaps").
Body(yamlBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create JSON over 3MB limit
t.Run("create should limit json parsing", func(t *testing.T) {
jsonBody := []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "mytest"
},
"values": ` + strings.Repeat("[", 3*1024*1024/2) + strings.Repeat("]", 3*1024*1024/2) + "}")
_, err := rest.Post().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
AbsPath("/api/v1/namespaces/default/configmaps").
Body(jsonBody).
DoRaw()
if !apierrors.IsRequestEntityTooLargeError(err) {
t.Errorf("expected too large error, got %v", err)
}
})
// Create JSON just under 3MB limit, nested
t.Run("create should handle a json document just under the maximum size with correct nesting", func(t *testing.T) {
jsonBody := []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "mytest"
},
"values": ` + strings.Repeat("[", 3*1024*1024/2-100) + strings.Repeat("]", 3*1024*1024/2-100) + "}")
_, err := rest.Post().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
AbsPath("/api/v1/namespaces/default/configmaps").
Body(jsonBody).
DoRaw()
// TODO(liggitt): expect bad request on deep nesting, rather than success on dropped unknown field data
if err != nil && !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
// Create JSON just under 3MB limit, not nested
t.Run("create should handle a json document just under the maximum size with unbalanced nesting", func(t *testing.T) {
jsonBody := []byte(`{
"apiVersion": "v1",
"kind": "ConfigMap",
"metadata": {
"name": "mytest"
},
"values": ` + strings.Repeat("[", 3*1024*1024-1000) + "}")
_, err := rest.Post().
SetHeader("Accept", "application/json").
SetHeader("Content-Type", "application/json").
AbsPath("/api/v1/namespaces/default/configmaps").
Body(jsonBody).
DoRaw()
if !apierrors.IsBadRequest(err) {
t.Errorf("expected bad request, got %v", err)
}
})
}

View File

@ -307,30 +307,36 @@ func TestObjectSizeResponses(t *testing.T) {
client := clientset.NewForConfigOrDie(&restclient.Config{Host: s.URL})
const DeploymentMegabyteSize = 100000
const DeploymentTwoMegabyteSize = 1000000
const DeploymentTwoMegabyteSize = 175000
const DeploymentThreeMegabyteSize = 250000
expectedMsgFor1MB := `etcdserver: request is too large`
expectedMsgFor2MB := `rpc error: code = ResourceExhausted desc = trying to send message larger than max`
expectedMsgFor3MB := `Request entity too large: limit is 3145728`
expectedMsgForLargeAnnotation := `metadata.annotations: Too long: must have at most 262144 characters`
deployment1 := constructBody("a", DeploymentMegabyteSize, "labels", t) // >1 MB file
deployment2 := constructBody("a", DeploymentTwoMegabyteSize, "labels", t) // >2 MB file
deployment1 := constructBody("a", DeploymentMegabyteSize, "labels", t) // >1 MB file
deployment2 := constructBody("a", DeploymentTwoMegabyteSize, "labels", t) // >2 MB file
deployment3 := constructBody("a", DeploymentThreeMegabyteSize, "labels", t) // >3 MB file
deployment3 := constructBody("a", DeploymentMegabyteSize, "annotations", t)
deployment4 := constructBody("a", DeploymentMegabyteSize, "annotations", t)
deployment4 := constructBody("sample/sample", DeploymentMegabyteSize, "finalizers", t) // >1 MB file
deployment5 := constructBody("sample/sample", DeploymentTwoMegabyteSize, "finalizers", t) // >2 MB file
deployment5 := constructBody("sample/sample", DeploymentMegabyteSize, "finalizers", t) // >1 MB file
deployment6 := constructBody("sample/sample", DeploymentTwoMegabyteSize, "finalizers", t) // >2 MB file
deployment7 := constructBody("sample/sample", DeploymentThreeMegabyteSize, "finalizers", t) // >3 MB file
requests := []struct {
size string
deploymentObject *appsv1.Deployment
expectedMessage string
}{
{"1 MB", deployment1, expectedMsgFor1MB},
{"2 MB", deployment2, expectedMsgFor2MB},
{"1 MB", deployment3, expectedMsgForLargeAnnotation},
{"1 MB", deployment4, expectedMsgFor1MB},
{"2 MB", deployment5, expectedMsgFor2MB},
{"1 MB labels", deployment1, expectedMsgFor1MB},
{"2 MB labels", deployment2, expectedMsgFor2MB},
{"3 MB labels", deployment3, expectedMsgFor3MB},
{"1 MB annotations", deployment4, expectedMsgForLargeAnnotation},
{"1 MB finalizers", deployment5, expectedMsgFor1MB},
{"2 MB finalizers", deployment6, expectedMsgFor2MB},
{"3 MB finalizers", deployment7, expectedMsgFor3MB},
}
for _, r := range requests {