CVE-2023-34236: Exposure of Sensitive Information to an Unauthorized Actor (CWE-200)
value: Weave GitOps Terraform Controller (aka Weave TF-controller) is a controller for Flux to reconcile Terraform resources in a GitOps way. A vulnerability has been identified in Weave GitOps Terraform Controller which could allow an authenticated remote attacker to view sensitive information. This vulnerability stems from Weave GitOps Terraform Runners (`tf-runner`), where sensitive data is inadvertently printed - potentially revealing sensitive user data in their pod logs. In particular, functions `tfexec.ShowPlan`, `tfexec.ShowPlanRaw`, and `tfexec.Output` are implicated when the `tfexec` object set its `Stdout` and `Stderr` to be `os.Stdout` and `os.Stderr`. An unauthorized remote attacker could exploit this vulnerability by accessing these prints of sensitive information, which may contain configurations or tokens that could be used to gain unauthorized control or access to resources managed by the Terraform controller. A successful exploit could allow the attacker to utilize this sensitive data, potentially leading to unauthorized access or control of the system. This vulnerability has been addressed in Weave GitOps Terraform Controller versions `v0.14.4` and `v0.15.0-rc.5`. Users are urged to upgrade to one of these versions to mitigate the vulnerability. As a temporary measure until the patch can be applied, users can add the environment variable `DISABLE_TF_LOGS` to the tf-runners via the runner pod template of the Terraform Custom Resource. This will prevent the logging of sensitive information and mitigate the risk of this vulnerability.
CWE description: The product exposes sensitive information to an actor that is not explicitly authorized to have access to that information.
CVSS v3 base score: 6.5
Published: 2023-07-14T22:15Z
Repository: https://github.com/weaveworks/tf-controller
Language: Go
Files changed: runner/server.go, runner/server_plan.go, runner/server_save_tfplan.go, runner/server_show_plan.go
Commit date: 2023-06-05 19:11:55 +0700
Commit message: fix logging in tf-runner

Signed-off-by: Chanwit Kaewkasi <chanwit@gmail.com>

Diff: diff --git a/runner/server.go b/runner/server.go
index 5a5db65..676ef75 100644
--- a/runner/server.go
+++ b/runner/server.go
@@ -168,10 +168,22 @@ func (r *TerraformRunnerServer) ProcessCliConfig(ctx context.Context, req *Proce
 		}
 	}
 	return &ProcessCliConfigReply{FilePath: tfrcFilepath}, nil
 }
 
+// initLogger sets up the logger for the terraform runner
+func (r *TerraformRunnerServer) initLogger(log logr.Logger) {
+	disableTestLogging := os.Getenv("DISABLE_TF_LOGS") == "1"
+	if !disableTestLogging {
+		r.tf.SetStdout(os.Stdout)
+		r.tf.SetStderr(os.Stderr)
+		if os.Getenv("ENABLE_SENSITIVE_TF_LOGS") == "1" {
+			r.tf.SetLogger(&LocalPrintfer{logger: log})
+		}
+	}
+}
+
 func (r *TerraformRunnerServer) NewTerraform(ctx context.Context, req *NewTerraformRequest) (*NewTerraformReply, error) {
 	r.InstanceID = req.GetInstanceID()
 	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
 	log.Info("creating new terraform", "workingDir", req.WorkingDir, "execPath", req.ExecPath)
 	tf, err := tfexec.NewTerraform(req.WorkingDir, req.ExecPath)
@@ -189,18 +201,12 @@ func (r *TerraformRunnerServer) NewTerraform(ctx context.Context, req *NewTerraf
 		return nil, err
 	}
 	// cache the Terraform resource when initializing
 	r.terraform = &terraform
 
-	disableTestLogging := os.Getenv("DISABLE_TF_LOGS") == "1"
-	if !disableTestLogging {
-		r.tf.SetStdout(os.Stdout)
-		r.tf.SetStderr(os.Stderr)
-		if os.Getenv("ENABLE_SENSITIVE_TF_LOGS") == "1" {
-			r.tf.SetLogger(&LocalPrintfer{logger: log})
-		}
-	}
+	// init default logger
+	r.initLogger(log)
 
 	return &NewTerraformReply{Id: r.InstanceID}, nil
 }
 
 func (r *TerraformRunnerServer) SetEnv(ctx context.Context, req *SetEnvRequest) (*SetEnvReply, error) {
diff --git a/runner/server_plan.go b/runner/server_plan.go
index 46e0e70..0012eca 100644
--- a/runner/server_plan.go
+++ b/runner/server_plan.go
@@ -2,16 +2,44 @@ package runner
 
 import (
 	"context"
 	"errors"
 	"fmt"
+	"io"
+
 	"github.com/hashicorp/terraform-exec/tfexec"
+	tfjson "github.com/hashicorp/terraform-json"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
 	"sigs.k8s.io/controller-runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
 )
 
+func (r *TerraformRunnerServer) tfShowPlanFile(ctx context.Context, planPath string, opts ...tfexec.ShowOption) (*tfjson.Plan, error) {
+	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
+
+	// This is the only place where we disable the logger
+	r.tf.SetStdout(io.Discard)
+	r.tf.SetStderr(io.Discard)
+
+	defer r.initLogger(log)
+
+	return r.tf.ShowPlanFile(ctx, planPath, opts...)
+}
+
+func (r *TerraformRunnerServer) tfShowPlanFileRaw(ctx context.Context, planPath string, opts ...tfexec.ShowOption) (string, error) {
+	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
+
+	// This is the only place where we disable the logger
+	r.tf.SetStdout(io.Discard)
+	r.tf.SetStderr(io.Discard)
+
+	defer r.initLogger(log)
+
+	return r.tf.ShowPlanFileRaw(ctx, planPath, opts...)
+}
+
 func (r *TerraformRunnerServer) Plan(ctx context.Context, req *PlanRequest) (*PlanReply, error) {
 	log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
 	log.Info("creating a plan")
 	ctx, cancel := context.WithCancel(ctx)
 	go func() {
@@ -66,11 +94,12 @@ func (r *TerraformRunnerServer) Plan(ctx context.Context, req *PlanRequest) (*Pl
 	}
 
 	planCreated := false
 	if req.Out != "" {
 		planCreated = true
-		plan, err := r.tf.ShowPlanFile(ctx, req.Out)
+
+		plan, err := r.tfShowPlanFile(ctx, req.Out)
 		if err != nil {
 			return nil, err
 		}
 
 		// This is the case when the plan is empty.
@@ -79,9 +108,10 @@ func (r *TerraformRunnerServer) Plan(ctx context.Context, req *PlanRequest) (*Pl
 			plan.ResourceChanges == nil &&
 			plan.PriorState == nil &&
 			plan.OutputChanges == nil {
 			planCreated = false
 		}
+
 	}
 
 	return &PlanReply{Message: "ok", Drifted: drifted, PlanCreated: planCreated}, nil
 }
diff --git a/runner/server_save_tfplan.go b/runner/server_save_tfplan.go
index 3951596..dfd95bb 100644
--- a/runner/server_save_tfplan.go
+++ b/runner/server_save_tfplan.go
@@ -13,15 +13,15 @@ import (
 	"github.com/weaveworks/tf-controller/utils"
 	"k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/types"
-	"sigs.k8s.io/controller-runtime"
+	ctrl "sigs.k8s.io/controller-runtime"
 )
 
 func (r *TerraformRunnerServer) SaveTFPlan(ctx context.Context, req *SaveTFPlanRequest) (*SaveTFPlanReply, error) {
-	log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
+	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
 	log.Info("save the plan")
 	if req.TfInstance != r.InstanceID {
 		err := fmt.Errorf("no TF instance found")
 		log.Error(err, "no terraform")
 		return nil, err
@@ -45,11 +45,11 @@ func (r *TerraformRunnerServer) SaveTFPlan(ctx context.Context, req *SaveTFPlanR
 	if err := r.writePlanAsSecret(ctx, req.Name, req.Namespace, log, planName, tfplan, "", req.Uuid); err != nil {
 		return nil, err
 	}
 
 	if r.terraform.Spec.StoreReadablePlan == "json" {
-		planObj, err := r.tf.ShowPlanFile(ctx, TFPlanName)
+		planObj, err := r.tfShowPlanFile(ctx, TFPlanName)
 		if err != nil {
 			log.Error(err, "unable to get the plan output for json")
 			return nil, err
 		}
 		jsonBytes, err := json.Marshal(planObj)
@@ -61,11 +61,11 @@ func (r *TerraformRunnerServer) SaveTFPlan(ctx context.Context, req *SaveTFPlanR
 		if err := r.writePlanAsSecret(ctx, req.Name, req.Namespace, log, planName, jsonBytes, ".json", req.Uuid); err != nil {
 			return nil, err
 		}
 
 	} else if r.terraform.Spec.StoreReadablePlan == "human" {
-		rawOutput, err := r.tf.ShowPlanFileRaw(ctx, TFPlanName)
+		rawOutput, err := r.tfShowPlanFileRaw(ctx, TFPlanName)
 		if err != nil {
 			log.Error(err, "unable to get the plan output for human")
 			return nil, err
 		}
 
diff --git a/runner/server_show_plan.go b/runner/server_show_plan.go
index b7d64fc..532e3d9 100644
--- a/runner/server_show_plan.go
+++ b/runner/server_show_plan.go
@@ -14,11 +14,11 @@ func (r *TerraformRunnerServer) ShowPlanFileRaw(ctx context.Context, req *ShowPl
 		err := fmt.Errorf("no TF instance found")
 		log.Error(err, "no terraform")
 		return nil, err
 	}
 
-	rawOutput, err := r.tf.ShowPlanFileRaw(ctx, req.Filename)
+	rawOutput, err := r.tfShowPlanFileRaw(ctx, req.Filename)
 	if err != nil {
 		log.Error(err, "unable to get the raw plan output")
 		return nil, err
 	}
 
@@ -32,11 +32,11 @@ func (r *TerraformRunnerServer) ShowPlanFile(ctx context.Context, req *ShowPlanF
 		err := fmt.Errorf("no TF instance found")
 		log.Error(err, "no terraform")
 		return nil, err
 	}
 
-	plan, err := r.tf.ShowPlanFile(ctx, req.Filename)
+	plan, err := r.tfShowPlanFile(ctx, req.Filename)
 	if err != nil {
 		log.Error(err, "unable to get the json plan output")
 		return nil, err
 	}
 

Vulnerable code: 	disableTestLogging := os.Getenv("DISABLE_TF_LOGS") == "1"
	if !disableTestLogging {
		r.tf.SetStdout(os.Stdout)
		r.tf.SetStderr(os.Stderr)
		if os.Getenv("ENABLE_SENSITIVE_TF_LOGS") == "1" {
			r.tf.SetLogger(&LocalPrintfer{logger: log})
		}
	}
		plan, err := r.tf.ShowPlanFile(ctx, req.Out)
	"sigs.k8s.io/controller-runtime"
	log := controllerruntime.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
		planObj, err := r.tf.ShowPlanFile(ctx, TFPlanName)
		rawOutput, err := r.tf.ShowPlanFileRaw(ctx, TFPlanName)
	rawOutput, err := r.tf.ShowPlanFileRaw(ctx, req.Filename)
	plan, err := r.tf.ShowPlanFile(ctx, req.Filename)
Fixed code: // initLogger sets up the logger for the terraform runner
func (r *TerraformRunnerServer) initLogger(log logr.Logger) {
	disableTestLogging := os.Getenv("DISABLE_TF_LOGS") == "1"
	if !disableTestLogging {
		r.tf.SetStdout(os.Stdout)
		r.tf.SetStderr(os.Stderr)
		if os.Getenv("ENABLE_SENSITIVE_TF_LOGS") == "1" {
			r.tf.SetLogger(&LocalPrintfer{logger: log})
		}
	}
}

	// init default logger
	r.initLogger(log)
	"io"

	tfjson "github.com/hashicorp/terraform-json"
	ctrl "sigs.k8s.io/controller-runtime"
func (r *TerraformRunnerServer) tfShowPlanFile(ctx context.Context, planPath string, opts ...tfexec.ShowOption) (*tfjson.Plan, error) {
	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)

	// This is the only place where we disable the logger
	r.tf.SetStdout(io.Discard)
	r.tf.SetStderr(io.Discard)

	defer r.initLogger(log)

	return r.tf.ShowPlanFile(ctx, planPath, opts...)
}

func (r *TerraformRunnerServer) tfShowPlanFileRaw(ctx context.Context, planPath string, opts ...tfexec.ShowOption) (string, error) {
	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)

	// This is the only place where we disable the logger
	r.tf.SetStdout(io.Discard)
	r.tf.SetStderr(io.Discard)

	defer r.initLogger(log)

	return r.tf.ShowPlanFileRaw(ctx, planPath, opts...)
}


		plan, err := r.tfShowPlanFile(ctx, req.Out)

	ctrl "sigs.k8s.io/controller-runtime"
	log := ctrl.LoggerFrom(ctx, "instance-id", r.InstanceID).WithName(loggerName)
		planObj, err := r.tfShowPlanFile(ctx, TFPlanName)
		rawOutput, err := r.tfShowPlanFileRaw(ctx, TFPlanName)
	rawOutput, err := r.tfShowPlanFileRaw(ctx, req.Filename)
	plan, err := r.tfShowPlanFile(ctx, req.Filename)