Skip to content

Commit 6a10436

Browse files
committed
fix(openapi): prevent dynamic scope leakage between OpenAPI schemas
1 parent 96b11a9 commit 6a10436

11 files changed

Lines changed: 303 additions & 66 deletions

File tree

providers/openapi/components.go

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ type Components struct {
2020
type ComponentParameters map[string]*ParameterRef
2121

2222
func (c *Components) parse(config *dynamic.Config, reader dynamic.Reader) error {
23-
if err := c.Schemas.Parse(config, reader); err != nil {
24-
return fmt.Errorf("parse components failed: %w", err)
23+
if c.Schemas != nil {
24+
for it := c.Schemas.Iter(); it.Next(); {
25+
if err := it.Value().Parse(config, reader); err != nil {
26+
return fmt.Errorf("parse components failed: parse schema '%s' failed: %w", it.Key(), err)
27+
}
28+
}
2529
}
30+
2631
if err := c.Responses.parse(config, reader); err != nil {
2732
return fmt.Errorf("parse components failed: %w", err)
2833
}

providers/openapi/config.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,13 @@ func (c *Config) Parse(config *dynamic.Config, reader dynamic.Reader) error {
114114
return nil
115115
}
116116

117-
if err := c.Components.parse(config, reader); err != nil {
117+
config.Scope.OpenIfNeeded(config.Info.Path())
118+
119+
if err := c.Paths.parse(config, reader); err != nil {
118120
return err
119121
}
120122

121-
if err := c.Paths.parse(config, reader); err != nil {
123+
if err := c.Components.parse(config, reader); err != nil {
122124
return err
123125
}
124126

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
package openapi_test
2+
3+
import (
4+
"mokapi/config/dynamic"
5+
"mokapi/config/dynamic/dynamictest"
6+
"mokapi/providers/openapi"
7+
json "mokapi/schema/json/schema"
8+
"net/http"
9+
"testing"
10+
11+
"github.com/stretchr/testify/require"
12+
"gopkg.in/yaml.v3"
13+
)
14+
15+
func TestConfig_DynamicSchema(t *testing.T) {
16+
testdata := []struct {
17+
name string
18+
data string
19+
test func(t *testing.T, c *openapi.Config)
20+
}{
21+
{
22+
name: "dynamic reference using $id",
23+
data: `
24+
openapi: "3.0.0"
25+
info:
26+
title: "Dynamic Schema"
27+
paths:
28+
/foo:
29+
get:
30+
responses:
31+
'200':
32+
content:
33+
'application/json':
34+
schema:
35+
$id: /foo
36+
$ref: '#/components/schemas/Response'
37+
$defs:
38+
content:
39+
$dynamicAnchor: T
40+
type: object
41+
/bar:
42+
get:
43+
responses:
44+
'200':
45+
content:
46+
'application/json':
47+
schema:
48+
$id: /bar
49+
$ref: '#/components/schemas/Response'
50+
$defs:
51+
content:
52+
$dynamicAnchor: T
53+
type: array
54+
components:
55+
schemas:
56+
Response:
57+
$defs:
58+
content:
59+
$dynamicAnchor: T
60+
not: true
61+
type: object
62+
properties:
63+
content:
64+
$dynamicRef: '#T'
65+
error:
66+
type: object
67+
`,
68+
test: func(t *testing.T, c *openapi.Config) {
69+
require.NotNil(t, c)
70+
foo := c.Paths["/foo"].Value.Get.Responses.GetResponse(http.StatusOK).Content["application/json"].Schema
71+
require.NotNil(t, foo)
72+
require.NotNil(t, foo.Properties)
73+
require.Equal(t, json.Types{"object"}, foo.Properties.Get("content").Type)
74+
75+
bar := c.Paths["/bar"].Value.Get.Responses.GetResponse(http.StatusOK).Content["application/json"].Schema
76+
require.NotNil(t, bar)
77+
require.NotNil(t, bar.Properties)
78+
require.Equal(t, json.Types{"array"}, bar.Properties.Get("content").Type)
79+
80+
},
81+
},
82+
{
83+
name: "dynamic reference without $id",
84+
data: `
85+
openapi: "3.0.0"
86+
info:
87+
title: "Dynamic Schema"
88+
paths:
89+
/foo:
90+
get:
91+
responses:
92+
'200':
93+
content:
94+
'application/json':
95+
schema:
96+
$ref: '#/components/schemas/Response'
97+
$defs:
98+
content:
99+
$dynamicAnchor: T
100+
type: object
101+
/bar:
102+
get:
103+
responses:
104+
'200':
105+
content:
106+
'application/json':
107+
schema:
108+
$id: /bar
109+
$ref: '#/components/schemas/Response'
110+
$defs:
111+
content:
112+
$dynamicAnchor: T
113+
type: array
114+
components:
115+
schemas:
116+
Response:
117+
$defs:
118+
content:
119+
$dynamicAnchor: T
120+
not: true
121+
type: object
122+
properties:
123+
content:
124+
$dynamicRef: '#T'
125+
error:
126+
type: object
127+
`,
128+
test: func(t *testing.T, c *openapi.Config) {
129+
require.NotNil(t, c)
130+
s := c.Paths["/foo"].Value.Get.Responses.GetResponse(http.StatusOK).Content["application/json"].Schema
131+
require.NotNil(t, s)
132+
require.NotNil(t, s.Properties)
133+
require.Equal(t, json.Types{"object"}, s.Properties.Get("content").Type)
134+
135+
bar := c.Paths["/bar"].Value.Get.Responses.GetResponse(http.StatusOK).Content["application/json"].Schema
136+
require.NotNil(t, bar)
137+
require.NotNil(t, bar.Properties)
138+
require.Equal(t, json.Types{"array"}, bar.Properties.Get("content").Type)
139+
},
140+
},
141+
}
142+
143+
t.Parallel()
144+
for _, tc := range testdata {
145+
tc := tc
146+
t.Run(tc.name, func(t *testing.T) {
147+
t.Parallel()
148+
149+
c := &openapi.Config{}
150+
err := yaml.Unmarshal([]byte(tc.data), c)
151+
require.NoError(t, err)
152+
err = c.Parse(&dynamic.Config{Data: c}, &dynamictest.Reader{})
153+
require.NoError(t, err)
154+
tc.test(t, c)
155+
})
156+
}
157+
}

providers/openapi/media_type.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,11 @@ func (m *MediaType) parse(config *dynamic.Config, reader dynamic.Reader) error {
2323
if m == nil {
2424
return nil
2525
}
26+
27+
scope := m.ContentType.String()
28+
config.OpenScope(scope)
29+
defer config.CloseScope()
30+
2631
if err := m.Schema.Parse(config, reader); err != nil {
2732
return fmt.Errorf("parse schema failed: %s", err)
2833
}

providers/openapi/parse_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
package openapi_test
22

33
import (
4-
"github.com/stretchr/testify/require"
54
"mokapi/config/dynamic"
65
"mokapi/config/dynamic/dynamictest"
76
"mokapi/providers/openapi"
87
"mokapi/providers/openapi/openapitest"
98
"mokapi/providers/openapi/schema"
109
"mokapi/providers/openapi/schema/schematest"
1110
"testing"
11+
12+
"github.com/stretchr/testify/require"
1213
)
1314

1415
func Test_Parse(t *testing.T) {
@@ -132,18 +133,19 @@ func Test_ParseAndPatch(t *testing.T) {
132133
t.Parallel()
133134
var target *openapi.Config
134135
for _, c := range tc.configs {
135-
err := c.Parse(&dynamic.Config{
136-
Info: dynamictest.NewConfigInfo(),
137-
Data: c,
138-
}, &dynamictest.Reader{})
139-
require.NoError(t, err)
140136
if target == nil {
141137
target = c
142138
} else {
143139
target.Patch(c)
144140
}
145141
}
146142

143+
err := target.Parse(&dynamic.Config{
144+
Info: dynamictest.NewConfigInfo(),
145+
Data: target,
146+
}, &dynamictest.Reader{})
147+
require.NoError(t, err)
148+
147149
tc.test(t, target)
148150
})
149151
}

providers/openapi/schema/apply.go

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -55,16 +55,22 @@ func (s *Schema) apply(ref *Schema) {
5555
}
5656

5757
if !s.isSet("items") {
58-
s.Items = ref.Items
58+
resolved := cloneSchema(ref.Items)
59+
s.Items = resolved
5960
}
6061
if !s.isSet("prefixItems") {
61-
s.PrefixItems = ref.PrefixItems
62+
for _, pi := range ref.PrefixItems {
63+
resolved := cloneSchema(pi)
64+
s.PrefixItems = append(s.PrefixItems, resolved)
65+
}
6266
}
6367
if !s.isSet("unevaluatedItems") {
64-
s.UnevaluatedItems = ref.UnevaluatedItems
68+
resolved := cloneSchema(ref.UnevaluatedItems)
69+
s.UnevaluatedItems = resolved
6570
}
6671
if !s.isSet("contains") {
67-
s.Contains = ref.Contains
72+
resolved := cloneSchema(ref.Contains)
73+
s.Contains = resolved
6874
}
6975
if !s.isSet("maxContains") {
7076
s.MaxContains = ref.MaxContains
@@ -88,11 +94,19 @@ func (s *Schema) apply(ref *Schema) {
8894
s.ShuffleItems = ref.ShuffleItems
8995
}
9096

91-
if !s.isSet("properties") {
92-
s.Properties = ref.Properties
97+
if !s.isSet("properties") && ref.Properties != nil {
98+
s.Properties = &Schemas{}
99+
for it := ref.Properties.Iter(); it.Next(); {
100+
resolved := cloneSchema(it.Value())
101+
s.Properties.Set(it.Key(), resolved)
102+
}
93103
}
94104
if !s.isSet("patternProperties") {
95-
s.PatternProperties = ref.PatternProperties
105+
s.PatternProperties = map[string]*Schema{}
106+
for name, pi := range ref.PatternProperties {
107+
resolved := cloneSchema(pi)
108+
s.PatternProperties[name] = resolved
109+
}
96110
}
97111
if !s.isSet("minProperties") {
98112
s.MinProperties = ref.MinProperties
@@ -115,10 +129,12 @@ func (s *Schema) apply(ref *Schema) {
115129
fmt.Print("")
116130
}
117131
if !s.isSet("unevaluatedProperties") {
118-
s.UnevaluatedProperties = ref.UnevaluatedProperties
132+
resolved := cloneSchema(ref.UnevaluatedProperties)
133+
s.UnevaluatedProperties = resolved
119134
}
120135
if !s.isSet("propertyNames") {
121-
s.PropertyNames = ref.PropertyNames
136+
resolved := cloneSchema(ref.PropertyNames)
137+
s.PropertyNames = resolved
122138
}
123139

124140
if !s.isSet("anyOf") {
@@ -190,3 +206,11 @@ func (s *Schema) isEmpty() bool {
190206
func (s *Schema) isSet(name string) bool {
191207
return s.m[name]
192208
}
209+
210+
func cloneSchema(s *Schema) *Schema {
211+
if s == nil {
212+
return nil
213+
}
214+
c := *s
215+
return &c
216+
}

providers/openapi/schema/patch.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -175,8 +175,6 @@ func (s *Schema) Patch(patch *Schema) {
175175
}
176176
}
177177
}
178-
179-
s.cm.Notify(s)
180178
}
181179

182180
func (x *Xml) patch(patch *Xml) {

0 commit comments

Comments
 (0)