fix: error on empty file-resolved secrets; assert no secret leakage in errors

This commit is contained in:
2026-08-20 00:38:23 +01:00
parent 27a8306cb3
commit 16cf665910
2 changed files with 82 additions and 3 deletions
+8 -3
View File
@@ -10,8 +10,9 @@ import (
// empty value returns empty with no error. A "env:NAME" value returns
// os.Getenv(NAME), erroring if the variable is unset or empty. A
// "file:/path" value returns the file's contents with surrounding
// whitespace trimmed, erroring if the file cannot be read. Any other value
// is returned unchanged as a literal.
// whitespace trimmed, erroring if the file cannot be read or its trimmed
// contents are empty — symmetric with the env: case. Any other value is
// returned unchanged as a literal.
//
// Error messages never include a resolved secret value — only the env var
// name or file path, which are config, not secrets.
@@ -31,7 +32,11 @@ func resolveSecret(v string) (string, error) {
if err != nil {
return "", fmt.Errorf("llmgateway: cannot read secret file %q: %w", path, err)
}
return strings.TrimSpace(string(b)), nil
val := strings.TrimSpace(string(b))
if val == "" {
return "", fmt.Errorf("llmgateway: secret file %q is empty or whitespace-only", path)
}
return val, nil
}
return v, nil
}
+74
View File
@@ -3,6 +3,7 @@ package traefikllmgateway
import (
"os"
"path/filepath"
"strings"
"testing"
)
@@ -13,6 +14,14 @@ func TestResolveSecret(t *testing.T) {
if err := os.WriteFile(fp, []byte("from-file\n"), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
emptyFP := filepath.Join(dir, "empty")
if err := os.WriteFile(emptyFP, []byte(""), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
blankFP := filepath.Join(dir, "blank")
if err := os.WriteFile(blankFP, []byte(" \n\t\n"), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
cases := []struct {
in string
@@ -24,6 +33,8 @@ func TestResolveSecret(t *testing.T) {
{"env:LLMGW_MISSING", "", true},
{"file:" + fp, "from-file", false},
{"file:" + fp + ".nope", "", true},
{"file:" + emptyFP, "", true},
{"file:" + blankFP, "", true},
{"", "", false},
}
for _, c := range cases {
@@ -35,3 +46,66 @@ func TestResolveSecret(t *testing.T) {
})
}
}
// TestResolveSecret_EmptyFile_ErrorNamesPathNotContents is the symmetry
// regression: env: and file: must both reject an empty resolved secret, and
// the error must name the path, never echo the (empty) contents.
func TestResolveSecret_EmptyFile_ErrorNamesPathNotContents(t *testing.T) {
dir := t.TempDir()
emptyFP := filepath.Join(dir, "empty")
if err := os.WriteFile(emptyFP, []byte(""), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
_, err := resolveSecret("file:" + emptyFP)
if err == nil {
t.Fatal("want error for empty file-resolved secret, got nil")
}
if !strings.Contains(err.Error(), emptyFP) {
t.Fatalf("want error to name the path %q, got %q", emptyFP, err.Error())
}
}
// TestResolveSecret_ErrorNeverLeaksSecretValue is a regression test: across
// env and file resolution, on both the success and the failure path, the
// returned error (when non-nil) must never contain the resolved secret
// value — only the env var name or file path, which are config, not secrets.
func TestResolveSecret_ErrorNeverLeaksSecretValue(t *testing.T) {
const envSecret = "super-secret-env-value"
const fileSecret = "super-secret-file-value"
t.Setenv("LLMGW_LEAK_T", envSecret)
dir := t.TempDir()
fp := filepath.Join(dir, "leak")
if err := os.WriteFile(fp, []byte(fileSecret+"\n"), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
emptyFP := filepath.Join(dir, "empty")
if err := os.WriteFile(emptyFP, []byte(""), 0o600); err != nil {
t.Fatalf("WriteFile: %v", err)
}
cases := []struct {
name string
in string
}{
{"env success", "env:LLMGW_LEAK_T"},
{"env failure (unset)", "env:LLMGW_LEAK_MISSING"},
{"file success", "file:" + fp},
{"file failure (empty)", "file:" + emptyFP},
{"file failure (missing)", "file:" + fp + ".nope"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
_, err := resolveSecret(c.in)
if err == nil {
return // success path: nothing was resolved, nothing to leak
}
for _, secret := range []string{envSecret, fileSecret} {
if strings.Contains(err.Error(), secret) {
t.Fatalf("error leaks secret value: %v", err)
}
}
})
}
}