Skip to content

feat(export): add --to-files to write per-path .env files#258

Open
lollinng wants to merge 2 commits into
Infisical:mainfrom
lollinng:feat/export-to-files
Open

feat(export): add --to-files to write per-path .env files#258
lollinng wants to merge 2 commits into
Infisical:mainfrom
lollinng:feat/export-to-files

Conversation

@lollinng

Copy link
Copy Markdown

Description

Adds a --to-files flag to infisical export. Closes Infisical/infisical#1068.

Today export writes a single stream (stdout, or one file via --output-file). In a monorepo you have to run a separate infisical export --path="apps/cli" > apps/cli/.env for every app and package to reflect the logical secret-folder layout into the physical filesystem. --to-files does that in one command, mirroring each logical path into the filesystem:

  • infisical export --path="apps/*" --to-files writes apps/cli/.env, apps/api/.env, apps/web/.env (one file per immediate subfolder of apps)
  • omitting --path exports every folder in the tree, one file per path
  • a concrete --path="/apps/cli" writes the single apps/cli/.env

The output filename follows --format (.env, secrets.json, secrets.yaml, ...). It reuses the existing formatters, util.GetAllFolders, and the per-path secret fetch, and is mutually exclusive with --output-file.

Type of change

  • New feature (non-breaking change which adds functionality)

How has this been tested?

The path-resolution logic is split into pure helpers (expandToFilePaths, mapPathToFile) so it can be unit-tested without a live backend. packages/cmd/export_to_files_test.go (testify table tests) covers: glob to immediate children only, root-glob to top-level (non-recursive), omitted/root to a full recursive walk, concrete path to itself, leading/trailing-slash normalization, and the per-format filename mapping (12 cases).

  • go build ./... passes
  • go test -vet=off -run 'TestExpandToFilePaths|TestMapPathToFile' ./packages/cmd/ passes (-vet=off only to skip a pre-existing go vet finding in run.go, unrelated to this change)
  • infisical export --help shows the new flag

The end-to-end fetch-and-write against a real project needs a live Infisical backend (the test/ integration suite uses real credentials), so the resolution/mapping logic lives behind injectable helpers that are unit-tested directly.

One thing worth a maintainer's eye: I read "if the option is not provided, it will create a .env for all the paths" as a full recursive walk, and apps/* as a single level of children. Happy to adjust the glob/recursion semantics if you'd prefer different behavior (e.g. an explicit --recursive).

Checklist

  • I have read the contributing guide and code of conduct

Add a --to-files flag to `infisical export` that writes one output file
per logical secret path, mirroring the folder path into the filesystem
(e.g. apps/cli/.env). `--path="apps/*"` targets the immediate subfolders
of a folder; omitting --path exports every folder in the tree. Reuses the
existing formatters and folder-listing API. --to-files is mutually
exclusive with --output-file.

Implements Infisical/infisical#1068

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@greptile-apps

greptile-apps Bot commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds a --to-files flag to infisical export that mirrors each logical Infisical folder path into a per-path .env (or format-appropriate) file on disk, replacing manual per-app export loops in monorepos.

  • export_to_files.go introduces expandToFilePaths (glob/recursive/concrete path resolution), walkFolderTree, mapPathToFile, and runExportToFiles which fetches secrets for each resolved path and writes them to disk.
  • export.go registers the --to-files flag and routes to runExportToFiles when it is set, enforcing mutual exclusion with --output-file.
  • export_to_files_test.go adds table-driven unit tests for the pure path-resolution helpers using a stub folder tree.

Confidence Score: 3/5

The new --to-files feature writes files to disk using folder names sourced directly from the Infisical API without sanitization, which needs to be addressed before merging.

The path construction in runExportToFiles uses folder.Name from the API response without any validation. Because filepath.Join resolves .. components, a server returning adversarial folder names could write secrets to arbitrary filesystem locations. Additionally, walkFolderTree has no cycle detection, so a cyclic server response would cause a stack overflow. Both issues are in the core new code path and need fixing before this is safe to ship.

packages/cmd/export_to_files.go — specifically the listChildren closure building paths from raw API folder names, and the recursive walkFolderTree function.

Security Review

  • Path traversal via server-controlled folder names (export_to_files.go:94): folder.Name values returned by the Infisical API are concatenated directly into a file path. Because filepath.Join resolves .. components, a server returning a folder name like ../../.ssh would cause the CLI to write secrets outside the current working directory. Folder names should be validated (e.g. reject names containing / or ..) before use in path construction.

Important Files Changed

Filename Overview
packages/cmd/export_to_files.go New file implementing --to-files logic; has a path traversal bug (server-controlled folder names written directly to disk), no cycle detection in recursive tree walk, and a redundant MkdirAll call.
packages/cmd/export.go Adds --to-files flag registration and dispatch; mutual-exclusion check with --output-file is correct and integration with existing flag parsing is straightforward.
packages/cmd/export_to_files_test.go Table-driven unit tests for path expansion and file mapping helpers; covers all documented cases but does not exercise path traversal or cyclic-tree inputs.

Reviews (1): Last reviewed commit: "feat(export): add --to-files to write pe..." | Re-trigger Greptile

Comment on lines +93 to +96
paths := make([]string, 0, len(folders))
for _, folder := range folders {
paths = append(paths, base+"/"+folder.Name)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security Path traversal via server-controlled folder names

folder.Name comes directly from the Infisical API and is concatenated into a path without any sanitization. filepath.Join resolves .. components, so a server returning a folder named .. or ../../etc would cause the CLI to write secret files outside the current working directory. For example, if the API returns Name: "../../.ssh", the resulting output path becomes ../../.ssh/.env, silently overwriting files in unintended locations.

Consider validating that folder.Name contains no path separators or .. components before appending it, e.g. using filepath.Base(folder.Name) to strip any directory components or explicitly rejecting names that contain / or ...

Comment on lines +51 to +65
func walkFolderTree(root string, listChildren func(parent string) ([]string, error)) ([]string, error) {
paths := []string{root}
children, err := listChildren(root)
if err != nil {
return nil, err
}
for _, child := range children {
descendants, err := walkFolderTree(child, listChildren)
if err != nil {
return nil, err
}
paths = append(paths, descendants...)
}
return paths, nil
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 No cycle detection in walkFolderTree

If the Infisical API returns a folder structure where a child path equals or prefixes an ancestor (e.g. listChildren("/apps") returns ["/apps"]), walkFolderTree recurses infinitely and will crash with a stack overflow. This could be triggered by a buggy or adversarial server response. A simple visited-set check on the path before recursing would prevent this.

Comment thread packages/cmd/export_to_files.go Outdated
Comment on lines +131 to +136
outputFile := mapPathToFile(path, format)
if dir := filepath.Dir(outputFile); dir != "." {
if err := os.MkdirAll(dir, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %w", dir, err)
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Redundant os.MkdirAll call

writeToFile (defined in ssh.go) already calls os.MkdirAll on the directory before writing. The explicit os.MkdirAll here is never needed and can be removed to avoid the duplicated logic.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Comment on lines +38 to +48
func expandToFilePaths(pattern string, listChildren func(parent string) ([]string, error)) ([]string, error) {
if strings.HasSuffix(strings.TrimSpace(pattern), "/*") {
parent := normalizeSecretPath(strings.TrimSuffix(strings.TrimSpace(pattern), "/*"))
return listChildren(parent)
}

path := normalizeSecretPath(pattern)
if path == "/" {
return walkFolderTree("/", listChildren)
}
return []string{path}, nil

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Glob patterns with * anywhere but the end are silently treated as concrete paths

Only the suffix /* is detected as a glob. A user supplying apps/*/web or */cli will not get an error — the pattern is passed through normalizeSecretPath and treated as a literal Infisical path, almost certainly returning zero secrets with no diagnostic message. Consider returning an error for patterns that contain * in a position other than the trailing /*.

Validate that server-returned folder names are safe path segments before
mirroring them into the filesystem (reject "..", ".", and path separators) so
a malicious or buggy API response cannot write secret files outside the target
directory. Add cycle detection to the recursive folder walk to prevent
infinite recursion on a cyclic folder graph. Drop the redundant MkdirAll
since writeToFile already creates parent directories.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@lollinng

Copy link
Copy Markdown
Author

Thanks for the thorough review. Addressed both P1s and the redundant-MkdirAll P2 in f41ab80:

  • Path traversal: server-returned folder names are now validated with isSafeFolderSegment before being mirrored into the filesystem; names that are ., .., empty, or contain a path separator are rejected with an error, so a malicious/buggy API response can't write outside the target dir.
  • Cycle detection: walkFolderTree now carries a visited-set, so a cyclic folder graph terminates instead of recursing infinitely (added TestExpandToFilePathsTerminatesOnCycle).
  • Redundant MkdirAll: dropped, since writeToFile already creates parent dirs.

On the P2 about a mid-pattern * (e.g. apps/*/config) being treated as a concrete path: that's intentional for this PR; the issue scopes the glob to a trailing /* (immediate children) plus the omitted-path recursive case. Happy to extend to deeper glob semantics if you'd like.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ENG-164] Export multiple .env files following logical paths

1 participant