From 3f49cda42cf986c5e74d8e22c5479c33ce4f0fbd Mon Sep 17 00:00:00 2001 From: Lukasz Raczylo Date: Thu, 20 Aug 2026 14:29:55 +0100 Subject: [PATCH] fix: reject dot-only config names MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit configNamePattern (^[a-zA-Z0-9._-]+$) permits "." and ".." as valid provider/mcpServers/agents names, since a bare dot is in the allowed character class. Both names read as directory-traversal segments once embedded as a path segment in the gateway's own routes (passthroughRoute's "/{provider}/*", targetRoute's "/mcp/{name}/*" and "/a2a/{name}/*"). Nothing in this package's current path handling mis-resolves them, but disallowing them at construction is cheap and removes any future dependence on that staying true. - providers.go: add dotOnlyConfigNames ({".", ".."}), checked in validateConfigName between the character-class check and the reserved-name check. - provider_openai_test.go: extend TestValidateConfigName with "." and ".." cases (rejected) across provider/mcpServers/agents kinds, plus "..." (accepted — the rule is an exact match on "." and "..", not "any dots-only string"). Gates: go test ./... -count=1, go test ./... -race -count=1, gofmt -l ., go vet ./..., make yaegi-check, golangci-lint run ./..., golangci-lint run --enable=gosec ./..., fieldalignment ./... — all clean. --- provider_openai_test.go | 17 ++++++++++++----- providers.go | 20 +++++++++++++++++--- 2 files changed, 29 insertions(+), 8 deletions(-) diff --git a/provider_openai_test.go b/provider_openai_test.go index 41acebc..6fb7413 100644 --- a/provider_openai_test.go +++ b/provider_openai_test.go @@ -666,11 +666,13 @@ func TestBuildAdapters(t *testing.T) { }) } -// TestValidateConfigName covers validateConfigName's character-set and -// reserved-name rules, shared by buildAdapters (provider names) and -// validateTargetURLs (mcpServers/agents names, mcp_a2a_test.go) — a -// provider, MCP server, or agent named "v1", "mcp", or "a2a" would shadow -// one of the gateway's own fixed top-level routes. +// TestValidateConfigName covers validateConfigName's character-set, +// dot-only-name, and reserved-name rules, shared by buildAdapters +// (provider names) and validateTargetURLs (mcpServers/agents names, +// mcp_a2a_test.go) — a provider, MCP server, or agent named "v1", "mcp", +// or "a2a" would shadow one of the gateway's own fixed top-level routes, +// and one named "." or ".." reads as a directory-traversal segment once +// embedded as a path segment in the gateway's own routes. func TestValidateConfigName(t *testing.T) { tests := []struct { kind string @@ -687,6 +689,11 @@ func TestValidateConfigName(t *testing.T) { {"provider", "a2a", true}, // reserved: A2A target-proxy prefix {"mcpServers", "v1", true}, // same reserved set applies to every kind {"agents", "a2a", true}, + {"provider", ".", true}, // dot-only: matches configNamePattern's character class but rejected anyway + {"provider", "..", true}, // dot-only: same + {"mcpServers", ".", true}, // dot-only rule applies to every kind + {"agents", "..", true}, // dot-only rule applies to every kind + {"provider", "...", false}, // three dots is NOT one of dotOnlyConfigNames — the rule is exact-match on "." and "..", not "any dots-only string" } for _, tt := range tests { t.Run(tt.kind+"/"+tt.name, func(t *testing.T) { diff --git a/providers.go b/providers.go index 14e4d1c..f23d918 100644 --- a/providers.go +++ b/providers.go @@ -207,13 +207,27 @@ var configNamePattern = regexp.MustCompile(`^[a-zA-Z0-9._-]+$`) // reachable — reject it here instead of silently shadowing it. var reservedConfigNames = map[string]bool{"v1": true, "mcp": true, "a2a": true} -// validateConfigName reports an error unless name matches configNamePattern -// and is not one of reservedConfigNames. kind labels the config section -// (e.g. "provider", "mcpServers", "agents") in the error message. +// dotOnlyConfigNames are the two names configNamePattern's character class +// permits (it allows ".") but that are rejected anyway: "." and ".." read +// as directory-traversal segments once embedded as a path segment in the +// gateway's own routes (passthroughRoute's "/{provider}/*", targetRoute's +// "/mcp/{name}/*" and "/a2a/{name}/*"), even though nothing in this +// package's own path handling currently mis-resolves them — disallowing +// them at construction is cheap, and removes any dependence on that +// staying true. +var dotOnlyConfigNames = map[string]bool{".": true, "..": true} + +// validateConfigName reports an error unless name matches configNamePattern, +// is not one of dotOnlyConfigNames, and is not one of reservedConfigNames. +// kind labels the config section (e.g. "provider", "mcpServers", "agents") +// in the error message. func validateConfigName(kind, name string) error { if !configNamePattern.MatchString(name) { return fmt.Errorf("llmgateway: %s name %q is invalid: must match %s", kind, name, configNamePattern.String()) } + if dotOnlyConfigNames[name] { + return fmt.Errorf("llmgateway: %s name %q is invalid: must not be \".\" or \"..\"", kind, name) + } if reservedConfigNames[name] { return fmt.Errorf("llmgateway: %s name %q is reserved and would shadow a gateway route", kind, name) }