mirror of
https://github.com/go-task/task.git
synced 2026-07-01 16:44:34 +00:00
fix: prevent secret variable leaks in summary, verbose and key ordering
- mask secret values in `task --summary` (commands and vars listing) - mask resolved value of dynamic (sh) secrets in verbose logs - use masked command for platform-skipped verbose log - allow `secret` key in any position in a var definition (not only first) - add `value` to the JSON schema var definition - skip masking pass when no secret is present and dedup mask helpers - document that the `secret` flag is not propagated to derived variables
This commit is contained in:
@@ -117,7 +117,12 @@ func printTaskCommands(l *logger.Logger, t *ast.Task) {
|
||||
isCommand := c.Cmd != ""
|
||||
l.Outf(logger.Default, " - ")
|
||||
if isCommand {
|
||||
l.Outf(logger.Yellow, "%s\n", c.Cmd)
|
||||
// Use the masked command so secret values are not leaked in summaries
|
||||
logCmd := c.LogCmd
|
||||
if logCmd == "" {
|
||||
logCmd = c.Cmd
|
||||
}
|
||||
l.Outf(logger.Yellow, "%s\n", logCmd)
|
||||
} else {
|
||||
l.Outf(logger.Green, "Task: %s\n", c.Task)
|
||||
}
|
||||
@@ -196,6 +201,11 @@ func printTaskEnv(l *logger.Logger, t *ast.Task) {
|
||||
// formatVarValue formats a variable value based on its type.
|
||||
// Handles static values, shell commands (sh:), references (ref:), and maps.
|
||||
func formatVarValue(v ast.Var) string {
|
||||
// Never expose secret variables in the summary, whatever their type
|
||||
if v.Secret {
|
||||
return "*****"
|
||||
}
|
||||
|
||||
// Shell command - check this first before Value
|
||||
// because dynamic vars may have both Sh and an empty Value
|
||||
if v.Sh != nil {
|
||||
|
||||
@@ -8,39 +8,19 @@ import (
|
||||
// This function uses the Go templater to resolve all variables ({{.VAR}}) while
|
||||
// masking secret ones as "*****".
|
||||
func MaskSecrets(cmdTemplate string, vars *ast.Vars) string {
|
||||
if vars == nil || vars.Len() == 0 {
|
||||
return cmdTemplate
|
||||
}
|
||||
|
||||
// Create a cache map with secrets masked
|
||||
maskedVars := vars.DeepCopy()
|
||||
for name, v := range maskedVars.All() {
|
||||
if v.Secret {
|
||||
// Replace secret value with mask
|
||||
maskedVars.Set(name, ast.Var{
|
||||
Value: "*****",
|
||||
Secret: true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// Use the templater to resolve the template with masked secrets
|
||||
cache := &Cache{Vars: maskedVars}
|
||||
result := Replace(cmdTemplate, cache)
|
||||
|
||||
// If there was an error, return the original template
|
||||
if cache.Err() != nil {
|
||||
return cmdTemplate
|
||||
}
|
||||
|
||||
return result
|
||||
return MaskSecretsWithExtra(cmdTemplate, vars, nil)
|
||||
}
|
||||
|
||||
// MaskSecretsWithExtra is like MaskSecrets but also resolves extra variables (e.g., loop vars).
|
||||
func MaskSecretsWithExtra(cmdTemplate string, vars *ast.Vars, extra map[string]any) string {
|
||||
if vars == nil || vars.Len() == 0 {
|
||||
// Still need to resolve extra vars even if no vars
|
||||
cache := &Cache{Vars: ast.NewVars()}
|
||||
if vars == nil {
|
||||
vars = ast.NewVars()
|
||||
}
|
||||
|
||||
// Fast path: if there are no secrets to mask, resolve the template directly
|
||||
// without the extra DeepCopy + masking pass.
|
||||
if !hasSecrets(vars) {
|
||||
cache := &Cache{Vars: vars}
|
||||
result := ReplaceWithExtra(cmdTemplate, cache, extra)
|
||||
if cache.Err() != nil {
|
||||
return cmdTemplate
|
||||
@@ -48,7 +28,7 @@ func MaskSecretsWithExtra(cmdTemplate string, vars *ast.Vars, extra map[string]a
|
||||
return result
|
||||
}
|
||||
|
||||
// Create a cache map with secrets masked
|
||||
// Create a copy with secret values masked, leaving the originals untouched.
|
||||
maskedVars := vars.DeepCopy()
|
||||
for name, v := range maskedVars.All() {
|
||||
if v.Secret {
|
||||
@@ -62,9 +42,20 @@ func MaskSecretsWithExtra(cmdTemplate string, vars *ast.Vars, extra map[string]a
|
||||
cache := &Cache{Vars: maskedVars}
|
||||
result := ReplaceWithExtra(cmdTemplate, cache, extra)
|
||||
|
||||
// If there was an error, return the original template
|
||||
if cache.Err() != nil {
|
||||
return cmdTemplate
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// hasSecrets reports whether any variable is marked as secret.
|
||||
func hasSecrets(vars *ast.Vars) bool {
|
||||
for _, v := range vars.All() {
|
||||
if v.Secret {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user