From 4ebe5c9977764c61ded2682e10ca34cc869f6214 Mon Sep 17 00:00:00 2001 From: Guillaume Lours Date: Fri, 18 Sep 2026 13:51:47 +0200 Subject: [PATCH] fix: reject non-mapping entries under networks and models The compose-spec schema allowed a key under the top-level networks and models blocks to bypass the patternProperties check, since additionalProperties wasn't set to false like volumes/secrets/configs already do. A malformed key such as "/" therefore skipped schema validation entirely and reached transformMaybeExternal with its raw scalar value, which panicked on an unchecked type assertion instead of surfacing a validation error. Align the networks and models schema entries with their siblings, and make transformMaybeExternal return an error instead of panicking if a schema gap lets a non-mapping value through again. Signed-off-by: Guillaume Lours --- loader/loader_test.go | 11 +++++++++++ loader/tests/models_test.go | 15 +++++++++++++++ schema/compose-spec.json | 2 ++ transform/external.go | 6 +++++- transform/external_test.go | 5 +++++ 5 files changed, 38 insertions(+), 1 deletion(-) diff --git a/loader/loader_test.go b/loader/loader_test.go index 75aaecb6a..b67535cec 100644 --- a/loader/loader_test.go +++ b/loader/loader_test.go @@ -1572,6 +1572,17 @@ networks: assert.ErrorContains(t, err, "networks.foo: name and external.name conflict; only use name") } +func TestLoadNetworkWithInvalidKey(t *testing.T) { + _, err := loadYAML(` +name: load-network-with-invalid-key +networks: + /: + driver: bridge +`) + + assert.ErrorContains(t, err, "networks additional properties '/' not allowed") +} + func TestLoadNetworkWithName(t *testing.T) { config, err := loadYAML(` name: load-network-with-name diff --git a/loader/tests/models_test.go b/loader/tests/models_test.go index bcd921e38..73bc63f53 100644 --- a/loader/tests/models_test.go +++ b/loader/tests/models_test.go @@ -23,8 +23,10 @@ package tests // runtime, referencing entries under the top-level `models` key" import ( + "context" "testing" + "github.com/compose-spec/compose-go/v2/loader" "github.com/compose-spec/compose-go/v2/types" "gotest.tools/v3/assert" ) @@ -74,3 +76,16 @@ models: assert.Equal(t, p.Jobs["test_mapping"].Models["foo"].EndpointVariable, "MODEL_URL") assert.Equal(t, p.Jobs["test_mapping"].Models["foo"].ModelVariable, "MODEL") } + +func TestModelsWithInvalidKey(t *testing.T) { + _, err := loader.LoadWithContext(context.TODO(), types.ConfigDetails{ + ConfigFiles: []types.ConfigFile{{Filename: "compose.yml", Content: []byte(` +name: test +models: + /: + model: ai/model +`)}}, + Environment: map[string]string{}, + }) + assert.ErrorContains(t, err, "models additional properties '/' not allowed") +} diff --git a/schema/compose-spec.json b/schema/compose-spec.json index c9db4d1cd..e4e473bbc 100644 --- a/schema/compose-spec.json +++ b/schema/compose-spec.json @@ -38,6 +38,7 @@ "$ref": "#/$defs/model" } }, + "additionalProperties": false, "description": "Language models that will be used by your application." }, "networks": { @@ -47,6 +48,7 @@ "$ref": "#/$defs/network" } }, + "additionalProperties": false, "description": "Networks that are shared among multiple services." }, "volumes": { diff --git a/transform/external.go b/transform/external.go index be718f03c..025deaa29 100644 --- a/transform/external.go +++ b/transform/external.go @@ -27,7 +27,11 @@ func transformMaybeExternal(data any, p tree.Path, ignoreParseError bool) (any, if data == nil { return nil, nil } - resource, err := transformMapping(data.(map[string]any), p, ignoreParseError) + m, ok := data.(map[string]any) + if !ok { + return nil, fmt.Errorf("%s: must be a mapping", p) + } + resource, err := transformMapping(m, p, ignoreParseError) if err != nil { return nil, err } diff --git a/transform/external_test.go b/transform/external_test.go index 96eae4308..7e15e614e 100644 --- a/transform/external_test.go +++ b/transform/external_test.go @@ -68,6 +68,11 @@ func TestExternalLegacy(t *testing.T) { }) } +func TestExternalNonMapping(t *testing.T) { + _, err := transformMaybeExternal(1, tree.NewPath("resources.test"), false) + assert.Error(t, err, "resources.test: must be a mapping") +} + func TestExternalLegacyNamed(t *testing.T) { ssh, err := transformMaybeExternal(map[string]any{ "external": map[string]any{