k8s.io/kube-openapi@v0.0.0-20240228011516-70dd3763d340/pkg/validation/spec/fuzz_test.go (about) 1 /* 2 Copyright 2022 The Kubernetes Authors. 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 package spec 18 19 import ( 20 "github.com/go-openapi/jsonreference" 21 "github.com/google/go-cmp/cmp" 22 fuzz "github.com/google/gofuzz" 23 ) 24 25 var SwaggerFuzzFuncs []interface{} = []interface{}{ 26 func(v *Responses, c fuzz.Continue) { 27 c.FuzzNoCustom(v) 28 if v.Default != nil { 29 // Check if we hit maxDepth and left an incomplete value 30 if v.Default.Description == "" { 31 v.Default = nil 32 v.StatusCodeResponses = nil 33 } 34 } 35 36 // conversion has no way to discern empty statusCodeResponses from 37 // nil, since "default" is always included in the map. 38 // So avoid empty responses list 39 if len(v.StatusCodeResponses) == 0 { 40 v.StatusCodeResponses = nil 41 } 42 }, 43 func(v *Operation, c fuzz.Continue) { 44 c.FuzzNoCustom(v) 45 46 if v != nil { 47 // force non-nil 48 v.Responses = &Responses{} 49 c.Fuzz(v.Responses) 50 51 v.Schemes = nil 52 if c.RandBool() { 53 v.Schemes = append(v.Schemes, "http") 54 } 55 56 if c.RandBool() { 57 v.Schemes = append(v.Schemes, "https") 58 } 59 60 if c.RandBool() { 61 v.Schemes = append(v.Schemes, "ws") 62 } 63 64 if c.RandBool() { 65 v.Schemes = append(v.Schemes, "wss") 66 } 67 68 // Gnostic unconditionally makes security values non-null 69 // So do not fuzz null values into the array. 70 for i, val := range v.Security { 71 if val == nil { 72 v.Security[i] = make(map[string][]string) 73 } 74 75 for k, v := range val { 76 if v == nil { 77 val[k] = make([]string, 0) 78 } 79 } 80 } 81 } 82 }, 83 func(v map[int]Response, c fuzz.Continue) { 84 n := 0 85 c.Fuzz(&n) 86 if n == 0 { 87 // Test that fuzzer is not at maxDepth so we do not 88 // end up with empty elements 89 return 90 } 91 92 // Prevent negative numbers 93 num := c.Intn(4) 94 for i := 0; i < num+2; i++ { 95 val := Response{} 96 c.Fuzz(&val) 97 98 val.Description = c.RandString() + "x" 99 v[100*(i+1)+c.Intn(100)] = val 100 } 101 }, 102 func(v map[string]PathItem, c fuzz.Continue) { 103 n := 0 104 c.Fuzz(&n) 105 if n == 0 { 106 // Test that fuzzer is not at maxDepth so we do not 107 // end up with empty elements 108 return 109 } 110 111 num := c.Intn(5) 112 for i := 0; i < num+2; i++ { 113 val := PathItem{} 114 c.Fuzz(&val) 115 116 // Ref params are only allowed in certain locations, so 117 // possibly add a few to PathItems 118 numRefsToAdd := c.Intn(5) 119 for i := 0; i < numRefsToAdd; i++ { 120 theRef := Parameter{} 121 c.Fuzz(&theRef.Refable) 122 123 val.Parameters = append(val.Parameters, theRef) 124 } 125 126 v["/"+c.RandString()] = val 127 } 128 }, 129 func(v *SchemaOrArray, c fuzz.Continue) { 130 *v = SchemaOrArray{} 131 // gnostic parser just doesn't support more 132 // than one Schema here 133 v.Schema = &Schema{} 134 c.Fuzz(&v.Schema) 135 136 }, 137 func(v *SchemaOrBool, c fuzz.Continue) { 138 *v = SchemaOrBool{} 139 140 if c.RandBool() { 141 v.Allows = c.RandBool() 142 } else { 143 v.Schema = &Schema{} 144 v.Allows = true 145 c.Fuzz(&v.Schema) 146 } 147 }, 148 func(v map[string]Response, c fuzz.Continue) { 149 n := 0 150 c.Fuzz(&n) 151 if n == 0 { 152 // Test that fuzzer is not at maxDepth so we do not 153 // end up with empty elements 154 return 155 } 156 157 // Response definitions are not allowed to 158 // be refs 159 for i := 0; i < c.Intn(5)+1; i++ { 160 resp := &Response{} 161 162 c.Fuzz(resp) 163 resp.Ref = Ref{} 164 resp.Description = c.RandString() + "x" 165 166 // Response refs are not vendor extensible by gnostic 167 resp.VendorExtensible.Extensions = nil 168 v[c.RandString()+"x"] = *resp 169 } 170 }, 171 func(v *Header, c fuzz.Continue) { 172 if v != nil { 173 c.FuzzNoCustom(v) 174 175 // descendant Items of Header may not be refs 176 cur := v.Items 177 for cur != nil { 178 cur.Ref = Ref{} 179 cur = cur.Items 180 } 181 } 182 }, 183 func(v *Ref, c fuzz.Continue) { 184 *v = Ref{} 185 v.Ref, _ = jsonreference.New("http://asd.com/" + c.RandString()) 186 }, 187 func(v *Response, c fuzz.Continue) { 188 *v = Response{} 189 if c.RandBool() { 190 v.Ref = Ref{} 191 v.Ref.Ref, _ = jsonreference.New("http://asd.com/" + c.RandString()) 192 } else { 193 c.Fuzz(&v.VendorExtensible) 194 c.Fuzz(&v.Schema) 195 c.Fuzz(&v.ResponseProps) 196 197 v.Headers = nil 198 v.Ref = Ref{} 199 200 n := 0 201 c.Fuzz(&n) 202 if n != 0 { 203 // Test that fuzzer is not at maxDepth so we do not 204 // end up with empty elements 205 num := c.Intn(4) 206 for i := 0; i < num; i++ { 207 if v.Headers == nil { 208 v.Headers = make(map[string]Header) 209 } 210 hdr := Header{} 211 c.Fuzz(&hdr) 212 if hdr.Type == "" { 213 // hit maxDepth, just abort trying to make haders 214 v.Headers = nil 215 break 216 } 217 v.Headers[c.RandString()+"x"] = hdr 218 } 219 } else { 220 v.Headers = nil 221 } 222 } 223 224 v.Description = c.RandString() + "x" 225 226 // Gnostic parses empty as nil, so to keep avoid putting empty 227 if len(v.Headers) == 0 { 228 v.Headers = nil 229 } 230 }, 231 func(v **Info, c fuzz.Continue) { 232 // Info is never nil 233 *v = &Info{} 234 c.FuzzNoCustom(*v) 235 236 (*v).Title = c.RandString() + "x" 237 }, 238 func(v *Extensions, c fuzz.Continue) { 239 // gnostic parser only picks up x- vendor extensions 240 numChildren := c.Intn(5) 241 for i := 0; i < numChildren; i++ { 242 if *v == nil { 243 *v = Extensions{} 244 } 245 (*v)["x-"+c.RandString()] = c.RandString() 246 } 247 }, 248 func(v *Swagger, c fuzz.Continue) { 249 c.FuzzNoCustom(v) 250 251 if v.Paths == nil { 252 // Force paths non-nil since it does not have omitempty in json tag. 253 // This means a perfect roundtrip (via json) is impossible, 254 // since we can't tell the difference between empty/unspecified paths 255 v.Paths = &Paths{} 256 c.Fuzz(v.Paths) 257 } 258 259 v.Swagger = "2.0" 260 261 // Gnostic support serializing ID at all 262 // unavoidable data loss 263 v.ID = "" 264 265 v.Schemes = nil 266 if c.RandUint64()%2 == 1 { 267 v.Schemes = append(v.Schemes, "http") 268 } 269 270 if c.RandUint64()%2 == 1 { 271 v.Schemes = append(v.Schemes, "https") 272 } 273 274 if c.RandUint64()%2 == 1 { 275 v.Schemes = append(v.Schemes, "ws") 276 } 277 278 if c.RandUint64()%2 == 1 { 279 v.Schemes = append(v.Schemes, "wss") 280 } 281 282 // Gnostic unconditionally makes security values non-null 283 // So do not fuzz null values into the array. 284 for i, val := range v.Security { 285 if val == nil { 286 v.Security[i] = make(map[string][]string) 287 } 288 289 for k, v := range val { 290 if v == nil { 291 val[k] = make([]string, 0) 292 } 293 } 294 } 295 }, 296 func(v *SecurityScheme, c fuzz.Continue) { 297 v.Description = c.RandString() + "x" 298 c.Fuzz(&v.VendorExtensible) 299 300 switch c.Intn(3) { 301 case 0: 302 v.Type = "basic" 303 case 1: 304 v.Type = "apiKey" 305 switch c.Intn(2) { 306 case 0: 307 v.In = "header" 308 case 1: 309 v.In = "query" 310 default: 311 panic("unreachable") 312 } 313 v.Name = "x" + c.RandString() 314 case 2: 315 v.Type = "oauth2" 316 317 switch c.Intn(4) { 318 case 0: 319 v.Flow = "accessCode" 320 v.TokenURL = "https://" + c.RandString() 321 v.AuthorizationURL = "https://" + c.RandString() 322 case 1: 323 v.Flow = "application" 324 v.TokenURL = "https://" + c.RandString() 325 case 2: 326 v.Flow = "implicit" 327 v.AuthorizationURL = "https://" + c.RandString() 328 case 3: 329 v.Flow = "password" 330 v.TokenURL = "https://" + c.RandString() 331 default: 332 panic("unreachable") 333 } 334 c.Fuzz(&v.Scopes) 335 default: 336 panic("unreachable") 337 } 338 }, 339 func(v *interface{}, c fuzz.Continue) { 340 *v = c.RandString() + "x" 341 }, 342 func(v *string, c fuzz.Continue) { 343 *v = c.RandString() + "x" 344 }, 345 func(v *ExternalDocumentation, c fuzz.Continue) { 346 v.Description = c.RandString() + "x" 347 v.URL = c.RandString() + "x" 348 }, 349 func(v *SimpleSchema, c fuzz.Continue) { 350 c.FuzzNoCustom(v) 351 352 switch c.Intn(5) { 353 case 0: 354 v.Type = "string" 355 case 1: 356 v.Type = "number" 357 case 2: 358 v.Type = "boolean" 359 case 3: 360 v.Type = "integer" 361 case 4: 362 v.Type = "array" 363 default: 364 panic("unreachable") 365 } 366 367 switch c.Intn(5) { 368 case 0: 369 v.CollectionFormat = "csv" 370 case 1: 371 v.CollectionFormat = "ssv" 372 case 2: 373 v.CollectionFormat = "tsv" 374 case 3: 375 v.CollectionFormat = "pipes" 376 case 4: 377 v.CollectionFormat = "" 378 default: 379 panic("unreachable") 380 } 381 382 // None of the types which include SimpleSchema in our definitions 383 // actually support "example" in the official spec 384 v.Example = nil 385 386 // unsupported by openapi 387 v.Nullable = false 388 }, 389 func(v *int64, c fuzz.Continue) { 390 c.Fuzz(v) 391 392 // Gnostic does not differentiate between 0 and non-specified 393 // so avoid using 0 for fuzzer 394 if *v == 0 { 395 *v = 1 396 } 397 }, 398 func(v *float64, c fuzz.Continue) { 399 c.Fuzz(v) 400 401 // Gnostic does not differentiate between 0 and non-specified 402 // so avoid using 0 for fuzzer 403 if *v == 0.0 { 404 *v = 1.0 405 } 406 }, 407 func(v *Parameter, c fuzz.Continue) { 408 if v == nil { 409 return 410 } 411 c.Fuzz(&v.VendorExtensible) 412 if c.RandBool() { 413 // body param 414 v.Description = c.RandString() + "x" 415 v.Name = c.RandString() + "x" 416 v.In = "body" 417 c.Fuzz(&v.Description) 418 c.Fuzz(&v.Required) 419 420 v.Schema = &Schema{} 421 c.Fuzz(&v.Schema) 422 423 } else { 424 c.Fuzz(&v.SimpleSchema) 425 c.Fuzz(&v.CommonValidations) 426 v.AllowEmptyValue = false 427 v.Description = c.RandString() + "x" 428 v.Name = c.RandString() + "x" 429 430 switch c.Intn(4) { 431 case 0: 432 // Header param 433 v.In = "header" 434 case 1: 435 // Form data param 436 v.In = "formData" 437 v.AllowEmptyValue = c.RandBool() 438 case 2: 439 // Query param 440 v.In = "query" 441 v.AllowEmptyValue = c.RandBool() 442 case 3: 443 // Path param 444 v.In = "path" 445 v.Required = true 446 default: 447 panic("unreachable") 448 } 449 450 // descendant Items of Parameter may not be refs 451 cur := v.Items 452 for cur != nil { 453 cur.Ref = Ref{} 454 cur = cur.Items 455 } 456 } 457 }, 458 func(v *Schema, c fuzz.Continue) { 459 if c.RandBool() { 460 // file schema 461 c.Fuzz(&v.Default) 462 c.Fuzz(&v.Description) 463 c.Fuzz(&v.Example) 464 c.Fuzz(&v.ExternalDocs) 465 466 c.Fuzz(&v.Format) 467 c.Fuzz(&v.ReadOnly) 468 c.Fuzz(&v.Required) 469 c.Fuzz(&v.Title) 470 v.Type = StringOrArray{"file"} 471 472 } else { 473 // normal schema 474 c.Fuzz(&v.SchemaProps) 475 c.Fuzz(&v.SwaggerSchemaProps) 476 c.Fuzz(&v.VendorExtensible) 477 // c.Fuzz(&v.ExtraProps) 478 // ExtraProps will not roundtrip - gnostic throws out 479 // unrecognized keys 480 } 481 482 // Not supported by official openapi v2 spec 483 // and stripped by k8s apiserver 484 v.ID = "" 485 v.AnyOf = nil 486 v.OneOf = nil 487 v.Not = nil 488 v.Nullable = false 489 v.AdditionalItems = nil 490 v.Schema = "" 491 v.PatternProperties = nil 492 v.Definitions = nil 493 v.Dependencies = nil 494 }, 495 } 496 497 var SwaggerDiffOptions = []cmp.Option{ 498 // cmp.Diff panics on Ref since jsonreference.Ref uses unexported fields 499 cmp.Comparer(func(a Ref, b Ref) bool { 500 return a.String() == b.String() 501 }), 502 }