Merge pull request #13582 from hashicorp/multi-line-secrets

BUG: Scrub multiline sensitive values from build output
pull/13588/head
Tanmay Jain 2 months ago committed by GitHub
commit a664835382
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -0,0 +1,46 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package command
import (
"path/filepath"
"runtime"
"strings"
"testing"
)
func TestBuildScrubsSensitiveMultilineShellLocalOutput(t *testing.T) {
templatePath := filepath.Join(testFixture("repro-sensitive-multiline"), testBuildSensitiveMultilineShellLocalFixture(runtime.GOOS))
c := &BuildCommand{
Meta: TestMetaFile(t),
}
if exitCode := c.Run([]string{templatePath}); exitCode != 0 {
out, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta)
t.Fatalf("build failed with exit code %d\nstdout: %q\nstderr: %q", exitCode, out, stderr)
}
out, stderr := GetStdoutAndErrFromTestMeta(t, c.Meta)
output := out + "\n" + stderr
secret := "line-one-secret\nline-two-secret\nline-three-secret"
if strings.Contains(output, secret) {
t.Fatalf("multiline sensitive value leaked to build output: %q", output)
}
if strings.Contains(output, "line-one-secret") {
t.Fatalf("sensitive line leaked to build output: %q", output)
}
if !strings.Contains(output, "<sensitive>") {
t.Fatalf("expected scrubbed output, got: %q", output)
}
}
func testBuildSensitiveMultilineShellLocalFixture(goos string) string {
if goos == "windows" {
return "multi-pwd.windows.pkr.hcl"
}
return "multi-pwd.unix.pkr.hcl"
}

@ -0,0 +1,19 @@
variable "secret_multiline" {
type = string
sensitive = true
default = "line-one-secret\nline-two-secret\nline-three-secret"
}
source "null" "example" {
communicator = "none"
}
build {
sources = ["sources.null.example"]
provisioner "shell-local" {
inline = [
"printf 'BEGIN\n%s\nEND\n' '${var.secret_multiline}'"
]
}
}

@ -0,0 +1,25 @@
variable "secret_multiline" {
type = string
sensitive = true
default = "line-one-secret\nline-two-secret\nline-three-secret"
}
source "null" "example" {
communicator = "none"
}
build {
sources = ["sources.null.example"]
provisioner "shell-local" {
tempfile_extension = ".ps1"
environment_vars = ["SECRET_MULTILINE=${var.secret_multiline}"]
execute_command = ["powershell.exe", "{{.Vars}} {{.Script}}"]
env_var_format = "$env:%s=\"%s\"; "
inline = [
"Write-Output 'BEGIN'",
"Write-Output $env:SECRET_MULTILINE",
"Write-Output 'END'"
]
}
}

@ -14,7 +14,6 @@ import (
"github.com/hashicorp/hcl/v2"
"github.com/hashicorp/hcl/v2/ext/dynblock"
"github.com/hashicorp/hcl/v2/hclparse"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer/internal/dag"
"github.com/hashicorp/packer/packer"
"github.com/zclconf/go-cty/cty"
@ -306,7 +305,7 @@ func filterVarsFromLogs(inputOrLocal Variables) {
value := variable.Value()
_ = cty.Walk(value, func(_ cty.Path, nested cty.Value) (bool, error) {
if nested.IsWhollyKnown() && !nested.IsNull() && nested.Type().Equals(cty.String) {
packersdk.LogSecretFilter.Set(nested.AsString())
packer.RegisterSecret(nested.AsString())
}
return true, nil
})

@ -171,7 +171,7 @@ func (core *Core) initialize() error {
return err
}
for _, secret := range core.secrets {
packersdk.LogSecretFilter.Set(secret)
RegisterSecret(secret)
}
// Go through and interpolate all the build names. We should be able

@ -0,0 +1,34 @@
// Copyright IBM Corp. 2013, 2025
// SPDX-License-Identifier: BUSL-1.1
package packer
import (
"strings"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
)
func RegisterSecret(secret string) {
if secret == "" {
return
}
secrets := map[string]struct{}{
secret: {},
}
normalized := strings.ReplaceAll(secret, "\r\n", "\n")
secrets[normalized] = struct{}{}
for _, line := range strings.Split(normalized, "\n") {
if line == "" {
continue
}
secrets[line] = struct{}{}
}
for value := range secrets {
packersdk.LogSecretFilter.Set(value)
}
}

@ -52,7 +52,7 @@ func (u *ColoredUi) Askf(query string, vals ...any) (string, error) {
}
func (u *ColoredUi) Say(message string) {
u.Ui.Say(u.colorize(message, u.Color, true))
u.Ui.Say(u.colorize(scrubSecrets(message), u.Color, true))
}
func (u *ColoredUi) Sayf(message string, vals ...any) {
@ -70,7 +70,7 @@ func (u *ColoredUi) Error(message string) {
color = UiColorRed
}
u.Ui.Error(u.colorize(message, color, true))
u.Ui.Error(u.colorize(scrubSecrets(message), color, true))
}
func (u *ColoredUi) Errorf(message string, vals ...any) {
@ -139,7 +139,7 @@ func (u *TargetedUI) Askf(query string, args ...any) (string, error) {
}
func (u *TargetedUI) Say(message string) {
u.Ui.Say(u.prefixLines(true, message))
u.Ui.Say(u.prefixLines(true, scrubSecrets(message)))
}
func (u *TargetedUI) Sayf(message string, args ...any) {
@ -152,7 +152,7 @@ func (u *TargetedUI) Message(message string) {
}
func (u *TargetedUI) Error(message string) {
u.Ui.Error(u.prefixLines(true, message))
u.Ui.Error(u.prefixLines(true, scrubSecrets(message)))
}
func (u *TargetedUI) Errorf(message string, args ...any) {
@ -235,10 +235,10 @@ func (u *MachineReadableUi) Machine(category string, args ...string) {
// Prepare the args
for i, v := range args {
// Use packersdk.LogSecretFilter to scrub out sensitive variables
args[i] = packersdk.LogSecretFilter.FilterString(args[i])
args[i] = strings.Replace(v, ",", "%!(PACKER_COMMA)", -1)
args[i] = strings.Replace(args[i], "\r", "\\r", -1)
args[i] = strings.Replace(args[i], "\n", "\\n", -1)
args[i] = scrubSecrets(v)
args[i] = strings.ReplaceAll(args[i], ",", "%!(PACKER_COMMA)")
args[i] = strings.ReplaceAll(args[i], "\r", "\\r")
args[i] = strings.ReplaceAll(args[i], "\n", "\\n")
}
argsString := strings.Join(args, ",")
@ -276,7 +276,7 @@ func (u *TimestampedUi) Askf(query string, args ...any) (string, error) {
}
func (u *TimestampedUi) Say(message string) {
u.Ui.Say(u.timestampLine(message))
u.Ui.Say(u.timestampLine(scrubSecrets(message)))
}
func (u *TimestampedUi) Sayf(message string, args ...any) {
@ -289,7 +289,7 @@ func (u *TimestampedUi) Message(message string) {
}
func (u *TimestampedUi) Error(message string) {
u.Ui.Error(u.timestampLine(message))
u.Ui.Error(u.timestampLine(scrubSecrets(message)))
}
func (u *TimestampedUi) Errorf(message string, args ...any) {
@ -307,3 +307,7 @@ func (u *TimestampedUi) TrackProgress(src string, currentSize, totalSize int64,
func (u *TimestampedUi) timestampLine(string string) string {
return fmt.Sprintf("%v: %v", time.Now().Format(time.RFC3339), string)
}
func scrubSecrets(message string) string {
return packersdk.LogSecretFilter.FilterString(message)
}

@ -5,6 +5,8 @@ package packer
import (
"bytes"
"io"
"log"
"strings"
"testing"
@ -146,6 +148,27 @@ func TestTargetedUI(t *testing.T) {
}
}
func TestTargetedUI_ScrubsMultilineSecrets(t *testing.T) {
bufferUi := testUi()
targetedUi := &TargetedUI{
Target: "null.example",
Ui: bufferUi,
}
secret := "line-one-secret\nline-two-secret\nline-three-secret"
packersdk.LogSecretFilter.Set(secret)
targetedUi.Say("BEGIN\n" + secret + "\nEND")
actual := readWriter(bufferUi)
if strings.Contains(actual, secret) {
t.Fatalf("secret leaked in output: %q", actual)
}
if !strings.Contains(actual, "==> null.example: <sensitive>") {
t.Fatalf("expected scrubbed output, got: %q", actual)
}
}
func TestColoredUi_ImplUi(t *testing.T) {
var raw interface{}
raw = &ColoredUi{}
@ -297,4 +320,45 @@ func TestMachineReadableUi(t *testing.T) {
if data != expected {
t.Fatalf("bad: %#v", data)
}
buf.Reset()
secret := "line-one-secret\nline-two-secret\nline-three-secret"
packersdk.LogSecretFilter.Set(secret)
ui.Machine("foo", secret)
data = buf.String()
if strings.Contains(data, "line-one-secret") {
t.Fatalf("secret leaked in machine-readable output: %q", data)
}
if !strings.Contains(data, "<sensitive>") {
t.Fatalf("expected scrubbed machine-readable output, got: %q", data)
}
}
func TestLoggerScrubsCombinedMultilineSecrets(t *testing.T) {
secret := "line-one-secret\nline-two-secret\nline-three-secret"
RegisterSecret(secret)
buf := new(bytes.Buffer)
packersdk.LogSecretFilter.SetOutput(buf)
oldWriter := log.Writer()
oldFlags := log.Flags()
defer log.SetOutput(oldWriter)
defer log.SetFlags(oldFlags)
defer packersdk.LogSecretFilter.SetOutput(io.Discard)
log.SetFlags(0)
log.SetOutput(&packersdk.LogSecretFilter)
log.Print("BEGIN\n" + secret + "\nEND")
output := buf.String()
if strings.Contains(output, secret) {
t.Fatalf("combined multiline secret leaked to logger output: %q", output)
}
if strings.Contains(output, "line-one-secret") {
t.Fatalf("multiline secret line leaked to logger output: %q", output)
}
if !strings.Contains(output, "<sensitive>") {
t.Fatalf("expected scrubbed logger output, got: %q", output)
}
}

Loading…
Cancel
Save