Ignore hidden entries during secrets bootstrap

This commit is contained in:
2026-09-21 13:26:01 +00:00
parent fbe7b61c79
commit b9425c9ae2
4 changed files with 49 additions and 5 deletions

View File

@@ -293,10 +293,13 @@ operation once before starting concurrent work. The selected path may itself be
symlink; ConfigKit resolves it to an existing canonical directory before examining symlink; ConfigKit resolves it to an existing canonical directory before examining
its immediate entries. its immediate entries.
The bootstrap enumerates at most 1,024 entries and sorts them by filename. Every The bootstrap enumerates at most 1,024 entries, including dot-prefixed entries.
entry must have an ASCII environment-variable name and be a regular, nonsymlink Entries whose names begin with `.` (files, directories, or symlinks, such as `.git`
file. Metadata for all entries is validated before file content is read or process and `.gitignore`) are ignored before name or metadata validation, are never opened,
state is changed. Existing environment variables win, including variables whose and are omitted from the report. Remaining entries are sorted by filename; each
must have an ASCII environment-variable name and be a regular, nonsymlink
file. Metadata for all remaining entries is validated before file content is read
or process state is changed. Existing environment variables win, including variables whose
value is empty; their files are not opened. Each eligible file is limited to 1 MiB, value is empty; their files are not opened. Each eligible file is limited to 1 MiB,
and eligible content is limited to 8 MiB in total. ConfigKit removes exactly one and eligible content is limited to 8 MiB in total. ConfigKit removes exactly one
terminal LF or CRLF, preserves all other whitespace, accepts an empty value, and terminal LF or CRLF, preserves all other whitespace, accepts an empty value, and

View File

@@ -130,6 +130,7 @@ type stagedSecret struct {
// LoadSecretsDir explicitly loads immediate regular files from directory into // LoadSecretsDir explicitly loads immediate regular files from directory into
// previously unset process environment variables. Applications should call it // previously unset process environment variables. Applications should call it
// once during startup, before concurrent work; Load never calls it. // once during startup, before concurrent work; Load never calls it.
// Dot-prefixed entries are ignored and omitted from the report.
func LoadSecretsDir(directory string) (*SecretsReport, error) { func LoadSecretsDir(directory string) (*SecretsReport, error) {
return loadSecretsDir(directory, secretsEnvironment{lookup: os.LookupEnv, set: os.Setenv}, secretsAccess{ return loadSecretsDir(directory, secretsEnvironment{lookup: os.LookupEnv, set: os.Setenv}, secretsAccess{
openDirectory: os.Open, openDirectory: os.Open,
@@ -168,13 +169,17 @@ func loadSecretsDir(directory string, environment secretsEnvironment, access sec
if closeErr != nil { if closeErr != nil {
return report, secretsFailure(CodeSecretsRead, "secrets directory could not be closed: "+strconv.Quote(canonical)) return report, secretsFailure(CodeSecretsRead, "secrets directory could not be closed: "+strconv.Quote(canonical))
} }
entryLimitExceeded := len(entries) > secretsEntryLimit
entries = slices.DeleteFunc(entries, func(entry fs.DirEntry) bool {
return strings.HasPrefix(entry.Name(), ".")
})
slices.SortFunc(entries, func(left, right fs.DirEntry) int { return strings.Compare(left.Name(), right.Name()) }) slices.SortFunc(entries, func(left, right fs.DirEntry) int { return strings.Compare(left.Name(), right.Name()) })
report.entries = make([]SecretFileResult, len(entries)) report.entries = make([]SecretFileResult, len(entries))
for index, entry := range entries { for index, entry := range entries {
name := entry.Name() name := entry.Name()
report.entries[index] = SecretFileResult{Name: name, Path: filepath.Join(canonical, name), Status: SecretFileNotApplied} report.entries[index] = SecretFileResult{Name: name, Path: filepath.Join(canonical, name), Status: SecretFileNotApplied}
} }
if len(entries) > secretsEntryLimit { if entryLimitExceeded {
return report, secretsFailure(CodeSecretsLimitExceeded, "secrets directory exceeds the 1024-entry limit: "+strconv.Quote(canonical)) return report, secretsFailure(CodeSecretsLimitExceeded, "secrets directory exceeds the 1024-entry limit: "+strconv.Quote(canonical))
} }

View File

@@ -64,6 +64,41 @@ func TestLoadSecretsDirLoadsCanonicalSortedValues(t *testing.T) {
} }
} }
func TestLoadSecretsDirIgnoresDotEntries(t *testing.T) {
directory := t.TempDir()
if err := os.Mkdir(filepath.Join(directory, ".git"), 0o700); err != nil {
t.Fatal(err)
}
writeSecretTestFile(t, directory, ".gitignore", "CANARY\x00")
writeSecretTestFile(t, directory, ".DS_Store", "CANARY\x00")
if runtime.GOOS != "windows" {
if err := os.Symlink(filepath.Join(directory, "missing"), filepath.Join(directory, ".hidden-link")); err != nil {
t.Fatal(err)
}
}
access := testSecretsAccess()
access.lstat = func(path string) (fs.FileInfo, error) {
if strings.HasPrefix(filepath.Base(path), ".") {
t.Fatalf("inspected ignored entry %q", path)
}
return os.Lstat(path)
}
environment := map[string]string{}
report, err := loadSecretsDir(directory, mapSecretsEnvironment(environment), access)
if err != nil || len(report.Entries()) != 0 || len(environment) != 0 {
t.Fatalf("hidden-only directory: error = %v, report = %#v, environment = %#v", err, report.Entries(), environment)
}
writeSecretTestFile(t, directory, "SECRET", "value\n")
report, err = loadSecretsDir(directory, mapSecretsEnvironment(environment), access)
if err != nil {
t.Fatal(err)
}
want := []SecretFileResult{{Name: "SECRET", Path: filepath.Join(canonicalTestPath(t, directory), "SECRET"), Status: SecretFileLoaded}}
if !reflect.DeepEqual(report.Entries(), want) || !reflect.DeepEqual(environment, map[string]string{"SECRET": "value"}) {
t.Fatalf("mixed directory: report = %#v, environment = %#v", report.Entries(), environment)
}
}
func TestLoadSecretsDirPreservesExistingWithoutReading(t *testing.T) { func TestLoadSecretsDirPreservesExistingWithoutReading(t *testing.T) {
directory := t.TempDir() directory := t.TempDir()
writeSecretTestFile(t, directory, "EMPTY_WINNER", strings.Repeat("CANARY", int(secretFileByteLimit))) writeSecretTestFile(t, directory, "EMPTY_WINNER", strings.Repeat("CANARY", int(secretFileByteLimit)))

View File

@@ -88,6 +88,7 @@ func (e *SecretsError) Diagnostics() []Diagnostic {
// LoadSecretsDir explicitly loads immediate regular files from directory into // LoadSecretsDir explicitly loads immediate regular files from directory into
// previously unset process environment variables. Applications should call it // previously unset process environment variables. Applications should call it
// once during startup, before concurrent work; Load never calls it. // once during startup, before concurrent work; Load never calls it.
// Dot-prefixed entries are ignored and omitted from the report.
func LoadSecretsDir(directory string) (*SecretsReport, error) { func LoadSecretsDir(directory string) (*SecretsReport, error) {
report, err := engine.LoadSecretsDir(directory) report, err := engine.LoadSecretsDir(directory)
result := &SecretsReport{value: report} result := &SecretsReport{value: report}