fix: trim values loaded from _FILE env vars in AppConfig (#1741)

This commit is contained in:
Alessandro (Ale) Segala
2026-09-07 19:10:54 -05:00
committed by GitHub
parent c65b77c980
commit 7ddc5d690b
5 changed files with 238 additions and 44 deletions
+18 -24
View File
@@ -390,36 +390,30 @@ func resolveFileBasedEnvVariable(field reflect.Value, fieldType reflect.StructFi
return nil
}
// Only process fields with the "env" tag
envTag := fieldType.Tag.Get("env")
if envTag == "" {
// Only process fields with the "env" tag, ignoring any option that follows the name of the variable
envVarName, _, _ := strings.Cut(fieldType.Tag.Get("env"), ",")
if envVarName == "" {
return nil
}
envVarName := envTag
if commaIndex := len(envTag); commaIndex > 0 {
envVarName = envTag[:commaIndex]
}
// If the file environment variable is not set, skip
envVarFileName := envVarName + "_FILE"
envVarFileValue := os.Getenv(envVarFileName)
if envVarFileValue == "" {
return nil
}
// #nosec G703 - Path is passed by the admin
fileContent, err := os.ReadFile(envVarFileValue)
if err != nil {
return fmt.Errorf("failed to read file for env var %s: %w", envVarFileName, err)
}
// Load the value from the file referenced by the "_FILE" variable, keeping the value parsed from the environment if that variable is not set
if isString {
field.SetString(strings.TrimSpace(string(fileContent)))
} else {
field.SetBytes(fileContent)
value, ok, err := LoadStringEnvVarFromFile(envVarName)
if err != nil || !ok {
return err
}
field.SetString(value)
return nil
}
value, ok, err := LoadEnvVarFromFile(envVarName)
if err != nil || !ok {
return err
}
field.SetBytes(value)
return nil
}
+41
View File
@@ -0,0 +1,41 @@
package common
import (
"fmt"
"os"
"strings"
)
// FileEnvVarSuffix is the suffix of the environment variable that contains the path to the file with the value of another environment variable
// For example, the value of "DB_CONNECTION_STRING" can be loaded from the file whose path is in "DB_CONNECTION_STRING_FILE"
const FileEnvVarSuffix = "_FILE"
// LoadEnvVarFromFile loads the value of the environment variable envVarName from the file referenced by the corresponding "_FILE" environment variable
// The second return value is false when the "_FILE" variable is not set, in which case the caller should use the value of the environment variable itself
// The content of the file is returned as-is: use LoadStringEnvVarFromFile for values that are used as strings
func LoadEnvVarFromFile(envVarName string) ([]byte, bool, error) {
fileEnvVarName := envVarName + FileEnvVarSuffix
fileName := os.Getenv(fileEnvVarName)
if fileName == "" {
return nil, false, nil
}
// #nosec G703 - Path is passed by the admin
fileContent, err := os.ReadFile(fileName)
if err != nil {
return nil, false, fmt.Errorf("failed to read file '%s' for env var %s: %w", fileName, fileEnvVarName, err)
}
return fileContent, true, nil
}
// LoadStringEnvVarFromFile is like LoadEnvVarFromFile, but returns the content of the file as a string, with leading and trailing whitespace removed
// Trimming is required because tools that write secrets to a file, including shell redirections and most editors, normally append a trailing newline
func LoadStringEnvVarFromFile(envVarName string) (string, bool, error) {
fileContent, ok, err := LoadEnvVarFromFile(envVarName)
if err != nil || !ok {
return "", false, err
}
return strings.TrimSpace(string(fileContent)), true, nil
}
+93
View File
@@ -0,0 +1,93 @@
package common
import (
"os"
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestLoadEnvVarFromFile(t *testing.T) {
t.Run("returns false when the file env var is not set", func(t *testing.T) {
value, ok, err := LoadEnvVarFromFile("TEST_SECRET")
require.NoError(t, err)
assert.False(t, ok)
assert.Empty(t, value)
})
t.Run("returns the content of the file as-is", func(t *testing.T) {
fileName := writeTempFile(t, "secret.txt", " my-secret\n")
t.Setenv("TEST_SECRET_FILE", fileName)
value, ok, err := LoadEnvVarFromFile("TEST_SECRET")
require.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, []byte(" my-secret\n"), value)
})
t.Run("returns an error when the file cannot be read", func(t *testing.T) {
t.Setenv("TEST_SECRET_FILE", filepath.Join(t.TempDir(), "does-not-exist.txt"))
_, ok, err := LoadEnvVarFromFile("TEST_SECRET")
require.Error(t, err)
require.ErrorContains(t, err, "TEST_SECRET_FILE")
assert.False(t, ok)
})
}
func TestLoadStringEnvVarFromFile(t *testing.T) {
t.Run("returns false when the file env var is not set", func(t *testing.T) {
value, ok, err := LoadStringEnvVarFromFile("TEST_SECRET")
require.NoError(t, err)
assert.False(t, ok)
assert.Empty(t, value)
})
t.Run("trims leading and trailing whitespace", func(t *testing.T) {
fileName := writeTempFile(t, "secret.txt", "\tmy-secret \r\n")
t.Setenv("TEST_SECRET_FILE", fileName)
value, ok, err := LoadStringEnvVarFromFile("TEST_SECRET")
require.NoError(t, err)
assert.True(t, ok)
assert.Equal(t, "my-secret", value)
})
t.Run("returns an empty value when the file is empty", func(t *testing.T) {
fileName := writeTempFile(t, "empty.txt", "\n")
t.Setenv("TEST_SECRET_FILE", fileName)
value, ok, err := LoadStringEnvVarFromFile("TEST_SECRET")
require.NoError(t, err)
assert.True(t, ok)
assert.Empty(t, value)
})
t.Run("returns an error when the file cannot be read", func(t *testing.T) {
t.Setenv("TEST_SECRET_FILE", filepath.Join(t.TempDir(), "does-not-exist.txt"))
_, ok, err := LoadStringEnvVarFromFile("TEST_SECRET")
require.Error(t, err)
assert.False(t, ok)
})
}
// writeTempFile writes content to a file in a temporary directory that is removed when the test ends, returning its path
func writeTempFile(t *testing.T, name string, content string) string {
t.Helper()
fileName := filepath.Join(t.TempDir(), name)
err := os.WriteFile(fileName, []byte(content), 0600)
require.NoError(t, err)
return fileName
}