From 66fdb420ceb808ea91b21a725b6b4297d6d116d5 Mon Sep 17 00:00:00 2001 From: cavidelizade Date: Fri, 17 Jul 2026 00:16:03 +0400 Subject: [PATCH] fix(api): reject a placeholder or too-short encryption key in production The production guard only checked that INSTANCE_ENCRYPTION_KEY was set, and .env.example ships it as "change-me-generate-a-random-key". Copying the example to a real host and setting ENV=production passed the guard while encrypting all instance secrets (SMTP password, OAuth client secrets, GitHub App private key) under a publicly known key. Reject a set-but-weak key too: the shipped placeholder and anything shorter than 16 characters now fail startup outside development. Closes #330 Co-Authored-By: Claude Opus 4.8 (1M context) --- apps/api/internal/config/config.go | 23 +++++++++++++++++++- apps/api/internal/config/config_test.go | 29 +++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 apps/api/internal/config/config_test.go diff --git a/apps/api/internal/config/config.go b/apps/api/internal/config/config.go index a0adb8e7..d8ac9b9d 100644 --- a/apps/api/internal/config/config.go +++ b/apps/api/internal/config/config.go @@ -109,17 +109,38 @@ func Load() (*Config, error) { if cfg.MagicCodeSecret == "" { missing = append(missing, "MAGIC_CODE_SECRET") } - if os.Getenv("INSTANCE_ENCRYPTION_KEY") == "" { + encKey := os.Getenv("INSTANCE_ENCRYPTION_KEY") + if encKey == "" { missing = append(missing, "INSTANCE_ENCRYPTION_KEY") } if len(missing) > 0 { return nil, fmt.Errorf("missing required production secrets %v (set ENV=development only for local dev)", missing) } + // A set-but-weak encryption key is as dangerous as a missing one: it + // feeds the AES key that protects instance secrets (SMTP password, OAuth + // client secrets, GitHub App private key). Reject the shipped placeholder + // and anything too short to be meaningfully random. + if isWeakSecret(encKey) { + return nil, fmt.Errorf("INSTANCE_ENCRYPTION_KEY is a placeholder or too short; set a random value of at least 16 characters for production") + } } return cfg, nil } +// isWeakSecret reports whether a production secret is unusable: a known +// .env.example placeholder, or too short to carry meaningful entropy. +func isWeakSecret(v string) bool { + if len(v) < 16 { + return true + } + switch v { + case "change-me-generate-a-random-key", "change-me", "changeme": + return true + } + return false +} + func getEnv(key, defaultVal string) string { if v := os.Getenv(key); v != "" { return v diff --git a/apps/api/internal/config/config_test.go b/apps/api/internal/config/config_test.go new file mode 100644 index 00000000..eff9eb38 --- /dev/null +++ b/apps/api/internal/config/config_test.go @@ -0,0 +1,29 @@ +package config + +import "testing" + +func TestIsWeakSecret(t *testing.T) { + weak := []string{ + "", + "short", + "change-me-generate-a-random-key", + "change-me", + "changeme", + "0123456789abcde", // 15 chars + } + for _, v := range weak { + if !isWeakSecret(v) { + t.Errorf("isWeakSecret(%q) = false; want true", v) + } + } + + strong := []string{ + "0123456789abcdef", // 16 chars + "a-perfectly-fine-random-key-value", // long, non-placeholder + } + for _, v := range strong { + if isWeakSecret(v) { + t.Errorf("isWeakSecret(%q) = true; want false", v) + } + } +}