From 837c45369931c99ed07350779a111b9cc6846610 Mon Sep 17 00:00:00 2001 From: Jon Langevin Date: Sun, 31 May 2026 22:34:48 -0400 Subject: [PATCH] fix: reject padded component refs --- protocol/types.go | 16 ++++++++++++---- protocol/types_test.go | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/protocol/types.go b/protocol/types.go index 7a2846e..f95861e 100644 --- a/protocol/types.go +++ b/protocol/types.go @@ -2307,10 +2307,14 @@ func (w WASMWorkload) Validate() error { } func validateProviderComponentRef(name, value string) error { - value = strings.TrimSpace(value) - if value == "" { + raw := value + trimmed := strings.TrimSpace(value) + if trimmed == "" { return fmt.Errorf("%s is required", name) } + if raw != trimmed { + return fmt.Errorf("%s must not contain surrounding whitespace", name) + } if strings.ContainsAny(value, " \t\r\n\x00") { return fmt.Errorf("%s must not contain whitespace or NUL", name) } @@ -4856,10 +4860,14 @@ func validateScopedRef(name, value, scheme string) error { } func validateComponentRef(name, value string) error { - value = strings.TrimSpace(value) - if value == "" { + raw := value + trimmed := strings.TrimSpace(value) + if trimmed == "" { return fmt.Errorf("%s is required", name) } + if raw != trimmed { + return fmt.Errorf("%s must not contain surrounding whitespace", name) + } for _, prefix := range []string{"artifact://", "content://", "provider://"} { if strings.HasPrefix(value, prefix) && !strings.ContainsAny(value, " \t\r\n\x00") { return nil diff --git a/protocol/types_test.go b/protocol/types_test.go index b293f40..2cc6747 100644 --- a/protocol/types_test.go +++ b/protocol/types_test.go @@ -152,6 +152,20 @@ func TestServiceWorkloadRejectsUnsafeShape(t *testing.T) { } } +func TestServiceWorkloadRejectsComponentRefWhitespace(t *testing.T) { + workload := protocol.ServiceWorkload{ + ComponentRef: " provider://workflow-plugin-compute-service/service-runtime", + ComponentDigest: "sha256:" + strings.Repeat("a", 64), + Command: []string{"serve", "--port", "8080"}, + Ports: []protocol.ServicePort{{Name: "http", Port: 8080, Protocol: "http"}}, + HealthCheck: protocol.HealthCheck{Kind: "http", Path: "/healthz", IntervalSeconds: 5, TimeoutSeconds: 2}, + Ingress: protocol.IngressPolicy{Mode: "private", AuthRequired: true}, + } + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "surrounding whitespace") { + t.Fatalf("service workload accepted whitespace-padded component_ref: %v", err) + } +} + func TestWorkloadSpecValidatesNodeServiceWorkload(t *testing.T) { endpoint := "node.example.invalid:30303" workload := protocol.WorkloadSpec{ @@ -184,6 +198,27 @@ func TestWorkloadSpecValidatesNodeServiceWorkload(t *testing.T) { } } +func TestNodeServiceWorkloadRejectsComponentRefWhitespace(t *testing.T) { + endpoint := "node.example.invalid:30303" + workload := protocol.NodeServiceWorkload{ + ComponentRef: "provider://workflow-plugin-compute-service/node-runtime ", + ComponentDigest: "sha256:" + strings.Repeat("b", 64), + Chain: "ethereum", + Network: "sepolia", + DataDirRef: "volume://nodes/sepolia", + RPCSecretRef: "secret://nodes/sepolia/rpc", + PeerPolicy: protocol.PeerPolicy{ + Mode: "allowlist", + AllowedPeers: []string{endpoint}, + EgressAllowlist: []string{endpoint}, + }, + HealthCheck: protocol.HealthCheck{Kind: "command", Command: []string{"node", "status"}, IntervalSeconds: 10, TimeoutSeconds: 3}, + } + if err := workload.Validate(); err == nil || !strings.Contains(err.Error(), "surrounding whitespace") { + t.Fatalf("node-service workload accepted whitespace-padded component_ref: %v", err) + } +} + func TestNodeServiceWorkloadRejectsMissingHealthAndUnsafePeers(t *testing.T) { workload := protocol.NodeServiceWorkload{ ImageRef: "ghcr.io/gocodealone/node@sha256:" + strings.Repeat("c", 64),