Skip to content

Commit ee32c44

Browse files
fix: fixed validation of reference names to prevent empty and illegal references (#100)
1 parent 92a27c3 commit ee32c44

File tree

2 files changed

+82
-4
lines changed

2 files changed

+82
-4
lines changed

references/reference.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,16 @@ import (
44
"errors"
55
"fmt"
66
"net/url"
7+
"regexp"
78
"strings"
89

910
"github.com/speakeasy-api/openapi/jsonpointer"
1011
)
1112

13+
// componentNameRegex matches valid OpenAPI component names according to the spec.
14+
// Component names must match: ^[a-zA-Z0-9\.\-_]+$
15+
var componentNameRegex = regexp.MustCompile(`^[a-zA-Z0-9.\-_]+$`)
16+
1217
type Reference string
1318

1419
var _ fmt.Stringer = (*Reference)(nil)
@@ -65,6 +70,48 @@ func (r Reference) Validate() error {
6570
if err := jp.Validate(); err != nil {
6671
return fmt.Errorf("invalid reference JSON pointer: %w", err)
6772
}
73+
74+
// Validate OpenAPI component references have valid component names
75+
if err := r.validateComponentReference(jp); err != nil {
76+
return err
77+
}
78+
}
79+
80+
return nil
81+
}
82+
83+
// validateComponentReference validates that component references have valid component names.
84+
// According to the OpenAPI spec, component names must match: ^[a-zA-Z0-9\.\-_]+$
85+
func (r Reference) validateComponentReference(jp jsonpointer.JSONPointer) error {
86+
jpStr := string(jp)
87+
88+
// Check if this is a component reference
89+
if !strings.HasPrefix(jpStr, "/components/") {
90+
return nil
91+
}
92+
93+
// Split the pointer into parts
94+
parts := strings.Split(strings.TrimPrefix(jpStr, "/"), "/")
95+
96+
// parts[0] is "components", parts[1] is the component type (schemas, parameters, etc.)
97+
// parts[2] should be the component name
98+
if len(parts) < 3 {
99+
// Reference ends at component type (e.g., #/components/schemas/)
100+
return errors.New("invalid reference: component name cannot be empty")
101+
}
102+
103+
componentName := parts[2]
104+
if componentName == "" {
105+
return errors.New("invalid reference: component name cannot be empty")
106+
}
107+
108+
// Unescape the component name before validating (JSON pointer escaping: ~0 = ~, ~1 = /)
109+
unescapedName := strings.ReplaceAll(componentName, "~1", "/")
110+
unescapedName = strings.ReplaceAll(unescapedName, "~0", "~")
111+
112+
// Validate component name matches the required pattern
113+
if !componentNameRegex.MatchString(unescapedName) {
114+
return fmt.Errorf("invalid reference: component name %q must match pattern ^[a-zA-Z0-9.\\-_]+$", unescapedName)
68115
}
69116

70117
return nil

references/reference_test.go

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,10 +46,6 @@ func TestReference_Validate_Success(t *testing.T) {
4646
name: "JSON pointer with array index",
4747
ref: "#/paths/~1users~1{id}/get/responses/200/content/application~1json/examples/0",
4848
},
49-
{
50-
name: "JSON pointer with escaped characters",
51-
ref: "#/components/schemas/User~1Profile/properties/user~0name",
52-
},
5349
{
5450
name: "file URI",
5551
ref: "file:///path/to/schema.yaml#/User",
@@ -103,6 +99,41 @@ func TestReference_Validate_Error(t *testing.T) {
10399
ref: "https://example .com/api.yaml#/User",
104100
expectError: "invalid reference URI",
105101
},
102+
{
103+
name: "empty component name - schemas",
104+
ref: "#/components/schemas/",
105+
expectError: "component name cannot be empty",
106+
},
107+
{
108+
name: "empty component name - parameters",
109+
ref: "#/components/parameters/",
110+
expectError: "component name cannot be empty",
111+
},
112+
{
113+
name: "empty component name - responses",
114+
ref: "#/components/responses/",
115+
expectError: "component name cannot be empty",
116+
},
117+
{
118+
name: "missing component name - schemas",
119+
ref: "#/components/schemas",
120+
expectError: "component name cannot be empty",
121+
},
122+
{
123+
name: "component name with space",
124+
ref: "#/components/schemas/User Schema",
125+
expectError: "must match pattern",
126+
},
127+
{
128+
name: "component name with special characters",
129+
ref: "#/components/schemas/User@Schema",
130+
expectError: "must match pattern",
131+
},
132+
{
133+
name: "component name starting with slash",
134+
ref: "#/components/schemas//UserSchema",
135+
expectError: "component name cannot be empty",
136+
},
106137
}
107138

108139
for _, tt := range tests {

0 commit comments

Comments
 (0)