[object Object]

← back to Cli Printing Press

fix(openapi): Swagger 2.0 detection + resource name sanitization

4934eb508b01127e0d98fcd8d53984d52152a30c · 2026-03-23 15:28:11 -0700 · Matt Van Horn

Add swagger: keyword detection to IsOpenAPI() for Swagger 2.0 specs.
Add sanitizeResourceName() to prevent path traversal from untrusted
OpenAPI specs by stripping dots, slashes, and trimming underscores.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

Files touched

Diff

commit 4934eb508b01127e0d98fcd8d53984d52152a30c
Author: Matt Van Horn <455140+mvanhorn@users.noreply.github.com>
Date:   Mon Mar 23 15:28:11 2026 -0700

    fix(openapi): Swagger 2.0 detection + resource name sanitization
    
    Add swagger: keyword detection to IsOpenAPI() for Swagger 2.0 specs.
    Add sanitizeResourceName() to prevent path traversal from untrusted
    OpenAPI specs by stripping dots, slashes, and trimming underscores.
    
    Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
---
 internal/openapi/detect.go      |  5 ++++-
 internal/openapi/parser.go      | 13 ++++++++++++-
 internal/openapi/parser_test.go | 33 +++++++++++++++++++++++++++++++++
 3 files changed, 49 insertions(+), 2 deletions(-)

diff --git a/internal/openapi/detect.go b/internal/openapi/detect.go
index 4045233d..aee5b450 100644
--- a/internal/openapi/detect.go
+++ b/internal/openapi/detect.go
@@ -11,5 +11,8 @@ func IsOpenAPI(data []byte) bool {
 	}
 
 	content := strings.ToLower(string(data))
-	return strings.Contains(content, "openapi:") || strings.Contains(content, "\"openapi\"")
+	return strings.Contains(content, "openapi:") ||
+		strings.Contains(content, "\"openapi\"") ||
+		strings.Contains(content, "swagger:") ||
+		strings.Contains(content, "\"swagger\"")
 }
diff --git a/internal/openapi/parser.go b/internal/openapi/parser.go
index f56d3c46..4883c550 100644
--- a/internal/openapi/parser.go
+++ b/internal/openapi/parser.go
@@ -836,7 +836,7 @@ func resourceNameFromPath(path, basePath string) string {
 	if isPathParamSegment(segments[0]) {
 		return ""
 	}
-	return toSnakeCase(segments[0])
+	return sanitizeResourceName(toSnakeCase(segments[0]))
 }
 
 func endpointCollisionSuffix(path, resourceName, basePath string) string {
@@ -962,6 +962,17 @@ func toSnakeCase(input string) string {
 	return strings.Trim(b.String(), "_")
 }
 
+func sanitizeResourceName(name string) string {
+	name = strings.ReplaceAll(name, ".", "")
+	name = strings.ReplaceAll(name, "/", "")
+	name = strings.ReplaceAll(name, "\\", "")
+	name = strings.Trim(name, "_")
+	if name == "" {
+		return ""
+	}
+	return name
+}
+
 func toKebabCase(input string) string {
 	var b strings.Builder
 	lastHyphen := true
diff --git a/internal/openapi/parser_test.go b/internal/openapi/parser_test.go
index d83610fd..c5cef31b 100644
--- a/internal/openapi/parser_test.go
+++ b/internal/openapi/parser_test.go
@@ -69,6 +69,13 @@ info:
 paths: {}
 `)
 	openAPIJSON := []byte(`{"openapi":"3.0.1","info":{"title":"Demo","version":"1.0.0"},"paths":{}}`)
+	swagger20YAML := []byte(`swagger: "2.0"
+info:
+  title: Demo
+  version: 1.0.0
+paths: {}
+`)
+	swagger20JSON := []byte(`{"swagger":"2.0","info":{"title":"Demo","version":"1.0.0"},"paths":{}}`)
 	internalYAML := []byte(`
 name: demo
 base_url: https://api.example.com
@@ -82,6 +89,8 @@ resources:
 
 	assert.True(t, IsOpenAPI(openAPIYAML))
 	assert.True(t, IsOpenAPI(openAPIJSON))
+	assert.True(t, IsOpenAPI(swagger20YAML))
+	assert.True(t, IsOpenAPI(swagger20JSON))
 	assert.False(t, IsOpenAPI(internalYAML))
 }
 
@@ -131,3 +140,27 @@ func runGo(t *testing.T, dir string, args ...string) {
 	output, err := cmd.CombinedOutput()
 	require.NoError(t, err, string(output))
 }
+
+func TestSanitizeResourceName(t *testing.T) {
+	tests := []struct {
+		input string
+		want  string
+	}{
+		{"users", "users"},
+		{"user-accounts", "user_accounts"},
+		{"../../../etc/passwd", "etc_passwd"},
+		{"foo/bar", "foo_bar"},
+		{"foo\\bar", "foo_bar"},
+		{"..", ""},
+		{".", ""},
+		{"___", ""},
+		{"normal_name", "normal_name"},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.input, func(t *testing.T) {
+			got := sanitizeResourceName(toSnakeCase(tt.input))
+			assert.Equal(t, tt.want, got)
+		})
+	}
+}

← 6011234c feat(openapi): integration tests + oneline template fix for  ·  back to Cli Printing Press  ·  docs(skill): add known-specs registry and spec-format refere c547fa57 →