Skip to content

Commit 668c9bc

Browse files
fix(config): sanitize dotenv errors avoiding data leaking (#3529)
* fix(config): sanitize dotenv errors avoiding data leaking * fix(config): add comment for newline error handling fallback * Apply suggestions from code review Co-authored-by: Han Qiao <[email protected]> * Update config_test.go --------- Co-authored-by: Han Qiao <[email protected]>
1 parent fc1926e commit 668c9bc

File tree

2 files changed

+117
-1
lines changed

2 files changed

+117
-1
lines changed

pkg/config/config.go

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -929,7 +929,33 @@ func loadDefaultEnv(env string) error {
929929

930930
func loadEnvIfExists(path string) error {
931931
if err := godotenv.Load(path); err != nil && !errors.Is(err, os.ErrNotExist) {
932-
return errors.Errorf("failed to load %s: %w", ".env", err)
932+
// If DEBUG=1, return the error as is for full debugability
933+
if viper.GetBool("DEBUG") {
934+
return errors.Errorf("failed to load %s: %w", path, err)
935+
}
936+
msg := err.Error()
937+
switch {
938+
case strings.HasPrefix(msg, "unexpected character"):
939+
// Try to extract the character, fallback to generic
940+
start := strings.Index(msg, "unexpected character \"")
941+
if start != -1 {
942+
start += len("unexpected character \"")
943+
end := strings.Index(msg[start:], "\"")
944+
if end != -1 {
945+
char := msg[start : start+end]
946+
return errors.Errorf("failed to parse environment file: %s (unexpected character '%s' in variable name)", path, char)
947+
}
948+
}
949+
return errors.Errorf("failed to parse environment file: %s (unexpected character in variable name)", path)
950+
case strings.HasPrefix(msg, "unterminated quoted value"):
951+
return errors.Errorf("failed to parse environment file: %s (unterminated quoted value)", path)
952+
// If the error message contains newlines, there is a high chance that the actual content of the
953+
// dotenv file is being leaked. In such cases, we return a generic error to avoid unwanted leaks in the logs
954+
case strings.Contains(msg, "\n"):
955+
return errors.Errorf("failed to parse environment file: %s (syntax error)", path)
956+
default:
957+
return errors.Errorf("failed to load %s: %w", path, err)
958+
}
933959
}
934960
return nil
935961
}

pkg/config/config_test.go

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,15 @@ package config
33
import (
44
"bytes"
55
_ "embed"
6+
"fmt"
7+
"os"
68
"path"
79
"strings"
810
"testing"
911
fs "testing/fstest"
1012

1113
"github.com/BurntSushi/toml"
14+
"github.com/spf13/viper"
1215
"github.com/stretchr/testify/assert"
1316
"github.com/stretchr/testify/require"
1417
)
@@ -525,3 +528,90 @@ func TestLoadFunctionErrorMessageParsing(t *testing.T) {
525528
assert.ErrorContains(t, err, `'functions[verify_jwt]' expected a map, got 'bool'`)
526529
})
527530
}
531+
532+
func TestLoadEnvIfExists(t *testing.T) {
533+
t.Run("returns nil when file does not exist", func(t *testing.T) {
534+
err := loadEnvIfExists("nonexistent.env")
535+
assert.NoError(t, err)
536+
})
537+
538+
t.Run("returns raw error when file exists but is malformed and DEBUG=1", func(t *testing.T) {
539+
// Set DEBUG=1
540+
t.Setenv("DEBUG", "1")
541+
viper.AutomaticEnv()
542+
543+
// Create a temporary file with malformed content
544+
tmpFile, err := os.CreateTemp("", "test-*.env")
545+
require.NoError(t, err)
546+
defer os.Remove(tmpFile.Name())
547+
548+
// Write malformed content
549+
_, err = tmpFile.WriteString("[invalid]\nvalue=secret_value\n")
550+
require.NoError(t, err)
551+
require.NoError(t, tmpFile.Close())
552+
553+
// Test loading the malformed file
554+
err = loadEnvIfExists(tmpFile.Name())
555+
// Should contain the raw error, including the secret value
556+
assert.ErrorContains(t, err, "unexpected character")
557+
assert.ErrorContains(t, err, "secret_value")
558+
})
559+
560+
t.Run("returns error when file exists but is malformed invalid character", func(t *testing.T) {
561+
// Create a temporary file with malformed content
562+
tmpFile, err := os.CreateTemp("", "test-*.env")
563+
require.NoError(t, err)
564+
defer os.Remove(tmpFile.Name())
565+
566+
// Write malformed content
567+
_, err = tmpFile.WriteString("[invalid]\nvalue=secret_value\n")
568+
require.NoError(t, err)
569+
require.NoError(t, tmpFile.Close())
570+
571+
// Test loading the malformed file
572+
err = loadEnvIfExists(tmpFile.Name())
573+
assert.ErrorContains(t, err, fmt.Sprintf("failed to parse environment file: %s (unexpected character '[' in variable name)", tmpFile.Name()))
574+
assert.NotContains(t, err.Error(), "secret_value")
575+
})
576+
577+
t.Run("returns error when file exists but is malformed unterminated quotes", func(t *testing.T) {
578+
// Create a temporary file with malformed content
579+
tmpFile, err := os.CreateTemp("", "test-*.env")
580+
require.NoError(t, err)
581+
defer os.Remove(tmpFile.Name())
582+
583+
// Write malformed content
584+
_, err = tmpFile.WriteString("value=\"secret_value\n")
585+
require.NoError(t, err)
586+
require.NoError(t, tmpFile.Close())
587+
588+
// Test loading the malformed file
589+
err = loadEnvIfExists(tmpFile.Name())
590+
assert.ErrorContains(t, err, fmt.Sprintf("failed to parse environment file: %s (unterminated quoted value)", tmpFile.Name()))
591+
assert.NotContains(t, err.Error(), "secret_value")
592+
})
593+
594+
t.Run("loads valid env file successfully", func(t *testing.T) {
595+
// Create a temporary file with valid content
596+
tmpFile, err := os.CreateTemp("", "test-*.env")
597+
require.NoError(t, err)
598+
defer os.Remove(tmpFile.Name())
599+
600+
// Write valid content
601+
_, err = tmpFile.WriteString("TEST_KEY=test_value\nANOTHER_KEY=another_value")
602+
require.NoError(t, err)
603+
require.NoError(t, tmpFile.Close())
604+
605+
// Test loading the valid file
606+
err = loadEnvIfExists(tmpFile.Name())
607+
assert.NoError(t, err)
608+
609+
// Verify environment variables were loaded
610+
assert.Equal(t, "test_value", os.Getenv("TEST_KEY"))
611+
assert.Equal(t, "another_value", os.Getenv("ANOTHER_KEY"))
612+
613+
// Clean up environment variables
614+
os.Unsetenv("TEST_KEY")
615+
os.Unsetenv("ANOTHER_KEY")
616+
})
617+
}

0 commit comments

Comments
 (0)