@@ -79,23 +79,80 @@ const (
7979// openapiSpec is the minimal slice of an openapi/swagger document we care about:
8080// the version banner, info block, top-level security and the path map. unknown
8181// fields are ignored by both json and yaml decoders.
82+ //
83+ // path items decode into a bare interface{} rather than a typed operation struct
84+ // because openapi 3.1 allows a path item to be a "$ref" to a shared item instead
85+ // of a set of operations. a strongly typed sibling map (map[string]rawOpsStruct)
86+ // makes both json and yaml fail the whole document the moment one path item is a
87+ // $ref string next to another path item with real get/post operations, which
88+ // silently drops an otherwise valid, enumerable spec. interface{} accepts either
89+ // shape without erroring, and operationSecurity below sorts out what's actually
90+ // an operation object.
8291type openapiSpec struct {
83- OpenAPI string `json:"openapi" yaml:"openapi"`
84- Swagger string `json:"swagger" yaml:"swagger"`
85- Info openapiInfo `json:"info" yaml:"info"`
86- Security []map [string ][]string `json:"security" yaml:"security"`
87- Paths map [string ]map [string ]rawOps `json:"paths" yaml:"paths"`
92+ OpenAPI string `json:"openapi" yaml:"openapi"`
93+ Swagger string `json:"swagger" yaml:"swagger"`
94+ Info openapiInfo `json:"info" yaml:"info"`
95+ Security []map [string ][]string `json:"security" yaml:"security"`
96+ Paths map [string ]map [string ]interface {} `json:"paths" yaml:"paths"`
8897}
8998
9099type openapiInfo struct {
91100 Title string `json:"title" yaml:"title"`
92101 Version string `json:"version" yaml:"version"`
93102}
94103
95- // rawOps captures the per-operation security block. a pointer so an absent block
96- // (inherit global) is distinct from an explicit empty one (security: [] = public).
97- type rawOps struct {
98- Security * []map [string ][]string `json:"security" yaml:"security"`
104+ // operationSecurity pulls the per-operation security block out of a decoded path
105+ // item entry. it reports present=false when the entry isn't an operation object
106+ // at all (a $ref path item, or a security key that was never declared), which the
107+ // caller treats as "inherit global" the same as an absent security key.
108+ func operationSecurity (op interface {}) (reqs []map [string ][]string , present bool ) {
109+ obj , ok := op .(map [string ]interface {})
110+ if ! ok {
111+ return nil , false
112+ }
113+ raw , ok := obj ["security" ]
114+ if ! ok {
115+ return nil , false
116+ }
117+ // a security key whose value isn't a list (null, or a malformed scalar or
118+ // object) is not a usable requirement block. treat it as absent and inherit
119+ // the global default rather than fabricate an anonymous high-severity finding
120+ // from garbage, which also matches how the old typed decoder handled a null.
121+ if _ , ok := raw .([]interface {}); ! ok {
122+ return nil , false
123+ }
124+ return toSecurityReqs (raw ), true
125+ }
126+
127+ // toSecurityReqs converts a decoded "security" value (a list of scheme->scopes
128+ // requirement objects) into the typed form securityAllowsAnonymous expects.
129+ // malformed entries are skipped rather than treated as a parse failure, since by
130+ // this point the document has already passed the openapi/swagger version check.
131+ func toSecurityReqs (raw interface {}) []map [string ][]string {
132+ arr , ok := raw .([]interface {})
133+ if ! ok {
134+ return nil
135+ }
136+ reqs := make ([]map [string ][]string , 0 , len (arr ))
137+ for _ , item := range arr {
138+ obj , ok := item .(map [string ]interface {})
139+ if ! ok {
140+ continue
141+ }
142+ req := make (map [string ][]string , len (obj ))
143+ for scheme , scopesRaw := range obj {
144+ scopesArr , _ := scopesRaw .([]interface {})
145+ scopes := make ([]string , 0 , len (scopesArr ))
146+ for _ , s := range scopesArr {
147+ if str , ok := s .(string ); ok {
148+ scopes = append (scopes , str )
149+ }
150+ }
151+ req [scheme ] = scopes
152+ }
153+ reqs = append (reqs , req )
154+ }
155+ return reqs
99156}
100157
101158// OpenAPI probes the candidate spec paths concurrently and, on the first hit,
@@ -294,8 +351,8 @@ func specToResult(spec *openapiSpec) *OpenAPIResult {
294351 }
295352 // an explicit block decides on its own; an absent one inherits global.
296353 var unauth bool
297- if op . Security != nil {
298- unauth = securityAllowsAnonymous (* op . Security )
354+ if reqs , present := operationSecurity ( op ); present {
355+ unauth = securityAllowsAnonymous (reqs )
299356 } else {
300357 unauth = globalAllowsAnon
301358 }
0 commit comments