From a407f35f0946c2589733b2fb34309921ab493690 Mon Sep 17 00:00:00 2001 From: luqueee Date: Fri, 4 Sep 2026 12:44:26 +0200 Subject: [PATCH 1/2] fix: reject unknown YAML config keys (#15) --- internal/config/config.go | 5 ++++- internal/config/config_test.go | 36 ++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/internal/config/config.go b/internal/config/config.go index c44bda0..da6ed0d 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -6,6 +6,7 @@ import ( "bytes" "errors" "fmt" + "io" "net" "net/url" "os" @@ -186,7 +187,9 @@ func Load(path string) (*Config, error) { return nil, fmt.Errorf("read config %q: %w", path, err) } - if err := yaml.Unmarshal(data, cfg); err != nil { + decoder := yaml.NewDecoder(bytes.NewReader(data)) + decoder.KnownFields(true) + if err := decoder.Decode(cfg); err != nil && !errors.Is(err, io.EOF) { return nil, fmt.Errorf("parse config %q: %w", path, err) } diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 9e960aa..49f2059 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -239,6 +239,42 @@ func TestLoad_PartialYAML_MergesWithDefaults(t *testing.T) { } } +func TestLoad_RejectsUnknownTopLevelKey(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "cfg.yaml") + content := "remote: dev@workstation\nauto_discoverd: true\n" + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write config: %v", err) + } + + _, err := Load(path) + if err == nil { + t.Fatal("Load() accepted an unknown top-level key") + } + if !strings.Contains(err.Error(), "auto_discoverd") { + t.Fatalf("Load() error = %v, want unknown key name", err) + } + if !strings.Contains(err.Error(), path) { + t.Fatalf("Load() error = %v, want config path", err) + } +} + +func TestLoad_EmptyYAMLKeepsDefaults(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "empty.yaml") + if err := os.WriteFile(path, nil, 0o644); err != nil { + t.Fatalf("write config: %v", err) + } + + cfg, err := Load(path) + if err != nil { + t.Fatalf("Load() error = %v, want empty YAML to keep defaults", err) + } + if cfg.LogLevel != Default().LogLevel || cfg.SSHPort != Default().SSHPort { + t.Fatalf("Load() defaults = log level %q, SSH port %d; want %q, %d", cfg.LogLevel, cfg.SSHPort, Default().LogLevel, Default().SSHPort) + } +} + func TestLoad_InvalidYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "bad.yaml") From ad91729ae197715c5bc775c3f72624bb24ff675a Mon Sep 17 00:00:00 2001 From: luqueee Date: Fri, 4 Sep 2026 12:52:08 +0200 Subject: [PATCH 2/2] fix: reject trailing YAML documents --- internal/config/config.go | 8 ++++++++ internal/config/config_test.go | 37 ++++++++++++++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/internal/config/config.go b/internal/config/config.go index da6ed0d..f2d781f 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -192,6 +192,14 @@ func Load(path string) (*Config, error) { if err := decoder.Decode(cfg); err != nil && !errors.Is(err, io.EOF) { return nil, fmt.Errorf("parse config %q: %w", path, err) } + var trailing yaml.Node + if err := decoder.Decode(&trailing); err != nil { + if !errors.Is(err, io.EOF) { + return nil, fmt.Errorf("parse config %q: %w", path, err) + } + } else { + return nil, fmt.Errorf("parse config %q: multiple YAML documents are not supported", path) + } // Keep existing clip_url-only configurations usable after the safer // loopback default was introduced. An explicit clip_listen, including diff --git a/internal/config/config_test.go b/internal/config/config_test.go index 49f2059..20422d5 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -275,6 +275,43 @@ func TestLoad_EmptyYAMLKeepsDefaults(t *testing.T) { } } +func TestLoad_RejectsTrailingYAMLDocument(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "multi.yaml") + content := "remote: dev@workstation\n---\nauto_discoverd: true\n" + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write config: %v", err) + } + + _, err := Load(path) + if err == nil { + t.Fatal("Load() accepted multiple YAML documents") + } + if !strings.Contains(err.Error(), "multiple YAML documents") { + t.Fatalf("Load() error = %v, want multiple-document error", err) + } + if !strings.Contains(err.Error(), path) { + t.Fatalf("Load() error = %v, want config path", err) + } +} + +func TestLoad_RejectsMalformedTrailingYAMLDocument(t *testing.T) { + dir := t.TempDir() + path := filepath.Join(dir, "multi-invalid.yaml") + content := "remote: dev@workstation\n---\nnot: [valid\n" + if err := os.WriteFile(path, []byte(content), 0o644); err != nil { + t.Fatalf("write config: %v", err) + } + + _, err := Load(path) + if err == nil { + t.Fatal("Load() accepted a malformed trailing YAML document") + } + if !strings.Contains(err.Error(), path) { + t.Fatalf("Load() error = %v, want config path", err) + } +} + func TestLoad_InvalidYAML(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "bad.yaml")