From ef75b8be7e46fd790af138f18bd699ac44d8abf3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= <peter.stibrany@grafana.com>
Date: Sat, 17 Apr 2021 08:15:20 +0200
Subject: [PATCH 1/3] Fix for CVE-2021-31232
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Improve alertmanager template checks.
* Added HTTP and TLS validation
* Use reflection to scan alertmanager config for validation
* Fix validation of template files.

Signed-off-by: Marco Pracucci <marco@pracucci.com>
Signed-off-by: Peter Štibraný <peter.stibrany@grafana.com>
---
 pkg/alertmanager/alertmanager.go     |   9 +-
 pkg/alertmanager/api.go              | 138 ++++++++++-
 pkg/alertmanager/api_test.go         | 343 ++++++++++++++++++++++++---
 pkg/alertmanager/multitenant.go      |  67 +++++-
 pkg/alertmanager/multitenant_test.go |  53 +++++
 5 files changed, 563 insertions(+), 47 deletions(-)

diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go
index 39b3cc7d67..9ed7e97f4d 100644
--- a/pkg/alertmanager/alertmanager.go
+++ b/pkg/alertmanager/alertmanager.go
@@ -209,10 +209,13 @@ func clusterWait(p *cluster.Peer, timeout time.Duration) func() time.Duration {
 // ApplyConfig applies a new configuration to an Alertmanager.
 func (am *Alertmanager) ApplyConfig(userID string, conf *config.Config, rawCfg string) error {
 	templateFiles := make([]string, len(conf.Templates))
-	if len(conf.Templates) > 0 {
-		for i, t := range conf.Templates {
-			templateFiles[i] = filepath.Join(am.cfg.DataDir, "templates", userID, t)
+	for i, t := range conf.Templates {
+		templateFilepath, err := safeTemplateFilepath(filepath.Join(am.cfg.DataDir, "templates", userID), t)
+		if err != nil {
+			return err
 		}
+
+		templateFiles[i] = templateFilepath
 	}
 
 	tmpl, err := template.FromGlobs(templateFiles...)
diff --git a/pkg/alertmanager/api.go b/pkg/alertmanager/api.go
index d2d3f09fa6..2fc82f1576 100644
--- a/pkg/alertmanager/api.go
+++ b/pkg/alertmanager/api.go
@@ -6,6 +6,7 @@ import (
 	"net/http"
 	"os"
 	"path/filepath"
+	"reflect"
 
 	"github.com/cortexproject/cortex/pkg/alertmanager/alerts"
 	"github.com/cortexproject/cortex/pkg/tenant"
@@ -13,8 +14,10 @@ import (
 
 	"github.com/go-kit/kit/log"
 	"github.com/go-kit/kit/log/level"
+	"github.com/pkg/errors"
 	"github.com/prometheus/alertmanager/config"
 	"github.com/prometheus/alertmanager/template"
+	commoncfg "github.com/prometheus/common/config"
 	"gopkg.in/yaml.v2"
 )
 
@@ -27,6 +30,11 @@ const (
 	errNoOrgID               = "unable to determine the OrgID"
 )
 
+var (
+	errPasswordFileNotAllowed = errors.New("setting password_file, bearer_token_file and credentials_file is not allowed")
+	errTLSFileNotAllowed      = errors.New("setting TLS ca_file, cert_file and key_file is not allowed")
+)
+
 // UserConfig is used to communicate a users alertmanager configs
 type UserConfig struct {
 	TemplateFiles      map[string]string `yaml:"template_files"`
@@ -146,28 +154,52 @@ func validateUserConfig(logger log.Logger, cfg alerts.AlertConfigDesc) error {
 		return err
 	}
 
+	// Validate the config recursively scanning it.
+	if err := validateAlertmanagerConfig(amCfg); err != nil {
+		return err
+	}
+
+	// Validate templates referenced in the alertmanager config.
+	for _, name := range amCfg.Templates {
+		if err := validateTemplateFilename(name); err != nil {
+			return err
+		}
+	}
+
+	// Validate template files.
+	for _, tmpl := range cfg.Templates {
+		if err := validateTemplateFilename(tmpl.Filename); err != nil {
+			return err
+		}
+	}
+
 	// Create templates on disk in a temporary directory.
 	// Note: This means the validation will succeed if we can write to tmp but
 	// not to configured data dir, and on the flipside, it'll fail if we can't write
 	// to tmpDir. Ignoring both cases for now as they're ultra rare but will revisit if
 	// we see this in the wild.
-	tmpDir, err := ioutil.TempDir("", "validate-config")
+	userTmpDir, err := ioutil.TempDir("", "validate-config-"+cfg.User)
 	if err != nil {
 		return err
 	}
-	defer os.RemoveAll(tmpDir)
+	defer os.RemoveAll(userTmpDir)
 
 	for _, tmpl := range cfg.Templates {
-		_, err := createTemplateFile(tmpDir, cfg.User, tmpl.Filename, tmpl.Body)
+		templateFilepath, err := safeTemplateFilepath(userTmpDir, tmpl.Filename)
 		if err != nil {
-			level.Error(logger).Log("msg", "unable to create template file", "err", err, "user", cfg.User)
-			return fmt.Errorf("unable to create template file '%s'", tmpl.Filename)
+			level.Error(logger).Log("msg", "unable to create template file path", "err", err, "user", cfg.User)
+			return err
+		}
+
+		if _, err = storeTemplateFile(templateFilepath, tmpl.Body); err != nil {
+			level.Error(logger).Log("msg", "unable to store template file", "err", err, "user", cfg.User)
+			return fmt.Errorf("unable to store template file '%s'", tmpl.Filename)
 		}
 	}
 
 	templateFiles := make([]string, len(amCfg.Templates))
 	for i, t := range amCfg.Templates {
-		templateFiles[i] = filepath.Join(tmpDir, "templates", cfg.User, t)
+		templateFiles[i] = filepath.Join(userTmpDir, t)
 	}
 
 	_, err = template.FromGlobs(templateFiles...)
@@ -182,3 +214,97 @@ func validateUserConfig(logger log.Logger, cfg alerts.AlertConfigDesc) error {
 
 	return nil
 }
+
+// validateAlertmanagerConfig recursively scans the input config looking for data types for which
+// we have a specific validation and, whenever encountered, it runs their validation. Returns the
+// first error or nil if validation succeeds.
+func validateAlertmanagerConfig(cfg interface{}) error {
+	v := reflect.ValueOf(cfg)
+	t := v.Type()
+
+	// Skip invalid, the zero value or a nil pointer (checked by zero value).
+	if !v.IsValid() || v.IsZero() {
+		return nil
+	}
+
+	// If the input config is a pointer then we need to get its value.
+	// At this point the pointer value can't be nil.
+	if v.Kind() == reflect.Ptr {
+		v = v.Elem()
+		t = v.Type()
+	}
+
+	// Check if the input config is a data type for which we have a specific validation.
+	// At this point the value can't be a pointer anymore.
+	switch t {
+	case reflect.TypeOf(commoncfg.HTTPClientConfig{}):
+		return validateReceiverHTTPConfig(v.Interface().(commoncfg.HTTPClientConfig))
+
+	case reflect.TypeOf(commoncfg.TLSConfig{}):
+		return validateReceiverTLSConfig(v.Interface().(commoncfg.TLSConfig))
+	}
+
+	// If the input config is a struct, recursively iterate on all fields.
+	if t.Kind() == reflect.Struct {
+		for i := 0; i < t.NumField(); i++ {
+			field := t.Field(i)
+			fieldValue := v.FieldByIndex(field.Index)
+
+			// Skip any field value which can't be converted to interface (eg. primitive types).
+			if fieldValue.CanInterface() {
+				if err := validateAlertmanagerConfig(fieldValue.Interface()); err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	if t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
+		for i := 0; i < v.Len(); i++ {
+			fieldValue := v.Index(i)
+
+			// Skip any field value which can't be converted to interface (eg. primitive types).
+			if fieldValue.CanInterface() {
+				if err := validateAlertmanagerConfig(fieldValue.Interface()); err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	if t.Kind() == reflect.Map {
+		for _, key := range v.MapKeys() {
+			fieldValue := v.MapIndex(key)
+
+			// Skip any field value which can't be converted to interface (eg. primitive types).
+			if fieldValue.CanInterface() {
+				if err := validateAlertmanagerConfig(fieldValue.Interface()); err != nil {
+					return err
+				}
+			}
+		}
+	}
+
+	return nil
+}
+
+// validateReceiverHTTPConfig validates the HTTP config and returns an error if it contains
+// settings not allowed by Cortex.
+func validateReceiverHTTPConfig(cfg commoncfg.HTTPClientConfig) error {
+	if cfg.BasicAuth != nil && cfg.BasicAuth.PasswordFile != "" {
+		return errPasswordFileNotAllowed
+	}
+	if cfg.BearerTokenFile != "" {
+		return errPasswordFileNotAllowed
+	}
+	return validateReceiverTLSConfig(cfg.TLSConfig)
+}
+
+// validateReceiverTLSConfig validates the TLS config and returns an error if it contains
+// settings not allowed by Cortex.
+func validateReceiverTLSConfig(cfg commoncfg.TLSConfig) error {
+	if cfg.CAFile != "" || cfg.CertFile != "" || cfg.KeyFile != "" {
+		return errTLSFileNotAllowed
+	}
+	return nil
+}
diff --git a/pkg/alertmanager/api_test.go b/pkg/alertmanager/api_test.go
index 4fe8df8811..c41366f518 100644
--- a/pkg/alertmanager/api_test.go
+++ b/pkg/alertmanager/api_test.go
@@ -9,11 +9,15 @@ import (
 	"net/http/httptest"
 	"testing"
 
-	"github.com/cortexproject/cortex/pkg/alertmanager/alerts"
-	"github.com/cortexproject/cortex/pkg/util"
-
+	"github.com/pkg/errors"
+	"github.com/prometheus/alertmanager/config"
+	commoncfg "github.com/prometheus/common/config"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/weaveworks/common/user"
+
+	"github.com/cortexproject/cortex/pkg/alertmanager/alerts"
+	"github.com/cortexproject/cortex/pkg/util"
 )
 
 func TestAMConfigValidationAPI(t *testing.T) {
@@ -24,7 +28,7 @@ func TestAMConfigValidationAPI(t *testing.T) {
 		err      error
 	}{
 		{
-			name: "It is not a valid payload without receivers",
+			name: "Should return error if the alertmanager config contains no receivers",
 			cfg: `
 alertmanager_config: |
   route:
@@ -37,7 +41,7 @@ alertmanager_config: |
 			err: fmt.Errorf("error validating Alertmanager config: undefined receiver \"default-receiver\" used in route"),
 		},
 		{
-			name: "It is valid",
+			name: "Should pass if the alertmanager config is valid",
 			cfg: `
 alertmanager_config: |
   route:
@@ -51,9 +55,27 @@ alertmanager_config: |
 `,
 		},
 		{
-			name: "It is not valid with paths in the template",
+			name: "Should return error if the config is empty due to wrong indentation",
 			cfg: `
 alertmanager_config: |
+route:
+  receiver: 'default-receiver'
+  group_wait: 30s
+  group_interval: 5m
+  repeat_interval: 4h
+  group_by: [cluster, alertname]
+receivers:
+  - name: default-receiver
+template_files:
+  "good.tpl": "good-templ"
+  "not/very/good.tpl": "bad-template"
+`,
+			err: fmt.Errorf("error validating Alertmanager config: configuration provided is empty, if you'd like to remove your configuration please use the delete configuration endpoint"),
+		},
+		{
+			name: "Should return error if the alertmanager config is empty due to wrong key",
+			cfg: `
+XWRONGalertmanager_config: |
   route:
     receiver: 'default-receiver'
     group_wait: 30s
@@ -64,12 +86,11 @@ alertmanager_config: |
     - name: default-receiver
 template_files:
   "good.tpl": "good-templ"
-  "not/very/good.tpl": "bad-template"
 `,
-			err: fmt.Errorf("error validating Alertmanager config: unable to create template file 'not/very/good.tpl'"),
+			err: fmt.Errorf("error validating Alertmanager config: configuration provided is empty, if you'd like to remove your configuration please use the delete configuration endpoint"),
 		},
 		{
-			name: "It is not valid with .",
+			name: "Should return error if the external template file name contains an absolute path",
 			cfg: `
 alertmanager_config: |
   route:
@@ -81,33 +102,31 @@ alertmanager_config: |
   receivers:
     - name: default-receiver
 template_files:
-  "good.tpl": "good-templ"
-  ".": "bad-template"
+  "/absolute/filepath": "a simple template"
 `,
-			err: fmt.Errorf("error validating Alertmanager config: unable to create template file '.'"),
+			err: fmt.Errorf(`error validating Alertmanager config: invalid template name "/absolute/filepath": the template name cannot contain any path`),
 		},
 		{
-			name: "It is not valid if the config is empty due to wrong indendatation",
+			name: "Should return error if the external template file name contains a relative path",
 			cfg: `
 alertmanager_config: |
-route:
-  receiver: 'default-receiver'
-  group_wait: 30s
-  group_interval: 5m
-  repeat_interval: 4h
-  group_by: [cluster, alertname]
-receivers:
-  - name: default-receiver
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
 template_files:
-  "good.tpl": "good-templ"
-  "not/very/good.tpl": "bad-template"
+  "../filepath": "a simple template"
 `,
-			err: fmt.Errorf("error validating Alertmanager config: configuration provided is empty, if you'd like to remove your configuration please use the delete configuration endpoint"),
+			err: fmt.Errorf(`error validating Alertmanager config: invalid template name "../filepath": the template name cannot contain any path`),
 		},
 		{
-			name: "It is not valid if the config is empty due to wrong key",
+			name: "Should return error if the external template file name is not a valid filename",
 			cfg: `
-XWRONGalertmanager_config: |
+alertmanager_config: |
   route:
     receiver: 'default-receiver'
     group_wait: 30s
@@ -118,9 +137,176 @@ XWRONGalertmanager_config: |
     - name: default-receiver
 template_files:
   "good.tpl": "good-templ"
-  "not/very/good.tpl": "bad-template"
+  ".": "bad-template"
 `,
-			err: fmt.Errorf("error validating Alertmanager config: configuration provided is empty, if you'd like to remove your configuration please use the delete configuration endpoint"),
+			err: fmt.Errorf("error validating Alertmanager config: unable to store template file '.'"),
+		},
+		{
+			name: "Should return error if the referenced template contains the root /",
+			cfg: `
+alertmanager_config: |
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
+  templates:
+    - "/"
+`,
+			err: fmt.Errorf(`error validating Alertmanager config: invalid template name "/": the template name cannot contain any path`),
+		},
+		{
+			name: "Should return error if the referenced template contains the root with repeated separators ///",
+			cfg: `
+alertmanager_config: |
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
+  templates:
+    - "///"
+`,
+			err: fmt.Errorf(`error validating Alertmanager config: invalid template name "///": the template name cannot contain any path`),
+		},
+		{
+			name: "Should return error if the referenced template contains an absolute path",
+			cfg: `
+alertmanager_config: |
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
+  templates:
+    - "/absolute/filepath"
+`,
+			err: fmt.Errorf(`error validating Alertmanager config: invalid template name "/absolute/filepath": the template name cannot contain any path`),
+		},
+		{
+			name: "Should return error if the referenced template contains a relative path",
+			cfg: `
+alertmanager_config: |
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
+  templates:
+    - "../filepath"
+`,
+			err: fmt.Errorf(`error validating Alertmanager config: invalid template name "../filepath": the template name cannot contain any path`),
+		},
+		{
+			name: "Should pass if the referenced template is valid filename",
+			cfg: `
+alertmanager_config: |
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
+  templates:
+    - "something.tmpl"
+`,
+		},
+		{
+			name: "Should return error if global HTTP password_file is set",
+			cfg: `
+alertmanager_config: |
+  global:
+    http_config:
+      basic_auth:
+        password_file: /secrets
+
+  route:
+    receiver: 'default-receiver'
+  receivers:
+    - name: default-receiver
+`,
+			err: errors.Wrap(errPasswordFileNotAllowed, "error validating Alertmanager config"),
+		},
+		{
+			name: "Should return error if global HTTP bearer_token_file is set",
+			cfg: `
+alertmanager_config: |
+  global:
+    http_config:
+      bearer_token_file: /secrets
+
+  route:
+    receiver: 'default-receiver'
+  receivers:
+    - name: default-receiver
+`,
+			err: errors.Wrap(errPasswordFileNotAllowed, "error validating Alertmanager config"),
+		},
+		{
+			name: "Should return error if receiver's HTTP password_file is set",
+			cfg: `
+alertmanager_config: |
+  receivers:
+    - name: default-receiver
+      webhook_configs:
+        - url: http://localhost
+          http_config:
+            basic_auth:
+              password_file: /secrets
+
+  route:
+    receiver: 'default-receiver'
+`,
+			err: errors.Wrap(errPasswordFileNotAllowed, "error validating Alertmanager config"),
+		},
+		{
+			name: "Should return error if receiver's HTTP bearer_token_file is set",
+			cfg: `
+alertmanager_config: |
+  receivers:
+    - name: default-receiver
+      webhook_configs:
+        - url: http://localhost
+          http_config:
+            bearer_token_file: /secrets
+
+  route:
+    receiver: 'default-receiver'
+`,
+			err: errors.Wrap(errPasswordFileNotAllowed, "error validating Alertmanager config"),
+		},
+		{
+			name: "should return error if template is wrong",
+			cfg: `
+alertmanager_config: |
+  route:
+    receiver: 'default-receiver'
+    group_wait: 30s
+    group_interval: 5m
+    repeat_interval: 4h
+    group_by: [cluster, alertname]
+  receivers:
+    - name: default-receiver
+  templates:
+    - "*.tmpl"
+template_files:
+  "test.tmpl": "{{ invalid Go template }}"
+`,
+			err: fmt.Errorf(`error validating Alertmanager config: template: test.tmpl:1: function "invalid" not defined`),
 		},
 	}
 
@@ -146,7 +332,6 @@ template_files:
 				require.Equal(t, http.StatusBadRequest, resp.StatusCode)
 				require.Equal(t, tc.err.Error()+"\n", string(body))
 			}
-
 		})
 	}
 }
@@ -165,3 +350,103 @@ func (noopAlertStore) SetAlertConfig(ctx context.Context, cfg alerts.AlertConfig
 func (noopAlertStore) DeleteAlertConfig(ctx context.Context, user string) error {
 	return nil
 }
+
+func TestValidateAlertmanagerConfig(t *testing.T) {
+	tests := map[string]struct {
+		input    interface{}
+		expected error
+	}{
+		"*HTTPClientConfig": {
+			input: &commoncfg.HTTPClientConfig{
+				BasicAuth: &commoncfg.BasicAuth{
+					PasswordFile: "/secrets",
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"HTTPClientConfig": {
+			input: commoncfg.HTTPClientConfig{
+				BasicAuth: &commoncfg.BasicAuth{
+					PasswordFile: "/secrets",
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"*TLSConfig": {
+			input: &commoncfg.TLSConfig{
+				CertFile: "/cert",
+			},
+			expected: errTLSFileNotAllowed,
+		},
+		"TLSConfig": {
+			input: commoncfg.TLSConfig{
+				CertFile: "/cert",
+			},
+			expected: errTLSFileNotAllowed,
+		},
+		"struct containing *HTTPClientConfig as direct child": {
+			input: config.GlobalConfig{
+				HTTPConfig: &commoncfg.HTTPClientConfig{
+					BasicAuth: &commoncfg.BasicAuth{
+						PasswordFile: "/secrets",
+					},
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"struct containing *HTTPClientConfig as nested child": {
+			input: config.Config{
+				Global: &config.GlobalConfig{
+					HTTPConfig: &commoncfg.HTTPClientConfig{
+						BasicAuth: &commoncfg.BasicAuth{
+							PasswordFile: "/secrets",
+						},
+					},
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"struct containing *HTTPClientConfig as nested child within a slice": {
+			input: config.Config{
+				Receivers: []*config.Receiver{{
+					Name: "test",
+					WebhookConfigs: []*config.WebhookConfig{{
+						HTTPConfig: &commoncfg.HTTPClientConfig{
+							BasicAuth: &commoncfg.BasicAuth{
+								PasswordFile: "/secrets",
+							},
+						},
+					}}},
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"map containing *HTTPClientConfig": {
+			input: map[string]*commoncfg.HTTPClientConfig{
+				"test": {
+					BasicAuth: &commoncfg.BasicAuth{
+						PasswordFile: "/secrets",
+					},
+				},
+			},
+			expected: errPasswordFileNotAllowed,
+		},
+		"map containing TLSConfig as nested child": {
+			input: map[string][]config.EmailConfig{
+				"test": {{
+					TLSConfig: commoncfg.TLSConfig{
+						CAFile: "/file",
+					},
+				}},
+			},
+			expected: errTLSFileNotAllowed,
+		},
+	}
+
+	for testName, testData := range tests {
+		t.Run(testName, func(t *testing.T) {
+			err := validateAlertmanagerConfig(testData.input)
+			assert.True(t, errors.Is(err, testData.expected))
+		})
+	}
+}
diff --git a/pkg/alertmanager/multitenant.go b/pkg/alertmanager/multitenant.go
index a636122ba1..0c8d1c27ee 100644
--- a/pkg/alertmanager/multitenant.go
+++ b/pkg/alertmanager/multitenant.go
@@ -11,6 +11,7 @@ import (
 	"net/url"
 	"os"
 	"path/filepath"
+	"strings"
 	"sync"
 	"time"
 
@@ -630,7 +631,12 @@ func (am *MultitenantAlertmanager) setConfig(cfg alerts.AlertConfigDesc) error {
 	var hasTemplateChanges bool
 
 	for _, tmpl := range cfg.Templates {
-		hasChanged, err := createTemplateFile(am.cfg.DataDir, cfg.User, tmpl.Filename, tmpl.Body)
+		templateFilepath, err := safeTemplateFilepath(filepath.Join(am.cfg.DataDir, "templates", cfg.User), tmpl.Filename)
+		if err != nil {
+			return err
+		}
+
+		hasChanged, err := storeTemplateFile(templateFilepath, tmpl.Body)
 		if err != nil {
 			return err
 		}
@@ -815,25 +821,68 @@ func (s StatusHandler) ServeHTTP(w http.ResponseWriter, req *http.Request) {
 	}
 }
 
-func createTemplateFile(dataDir, userID, fn, content string) (bool, error) {
-	if fn != filepath.Base(fn) {
-		return false, fmt.Errorf("template file name '%s' is not not valid", fn)
+// validateTemplateFilename validated the template filename and returns error if it's not valid.
+// The validation done in this function is a first fence to avoid having a tenant submitting
+// a config which may escape the per-tenant data directory on disk.
+func validateTemplateFilename(filename string) error {
+	if filepath.Base(filename) != filename {
+		return fmt.Errorf("invalid template name %q: the template name cannot contain any path", filename)
+	}
+
+	// Further enforce no path in the template name.
+	if filepath.Dir(filepath.Clean(filename)) != "." {
+		return fmt.Errorf("invalid template name %q: the template name cannot contain any path", filename)
 	}
 
-	dir := filepath.Join(dataDir, "templates", userID, filepath.Dir(fn))
+	return nil
+}
+
+// safeTemplateFilepath builds and return the template filepath within the provided dir.
+// This function also performs a security check to make sure the provided templateName
+// doesn't contain a relative path escaping the provided dir.
+func safeTemplateFilepath(dir, templateName string) (string, error) {
+	// We expect all template files to be stored and referenced within the provided directory.
+	containerDir, err := filepath.Abs(dir)
+	if err != nil {
+		return "", err
+	}
+
+	// Build the actual path of the template.
+	actualPath, err := filepath.Abs(filepath.Join(containerDir, templateName))
+	if err != nil {
+		return "", err
+	}
+
+	// Ensure the actual path of the template is within the expected directory.
+	// This check is a counter-measure to make sure the tenant is not trying to
+	// escape its own directory on disk.
+	if !strings.HasPrefix(actualPath, containerDir) {
+		return "", fmt.Errorf("invalid template name %q: the template filepath is escaping the per-tenant local directory", templateName)
+	}
+
+	return actualPath, nil
+}
+
+// storeTemplateFile stores template file at the given templateFilepath.
+// Returns true, if file content has changed (new or updated file), false if file with the same name
+// and content was already stored locally.
+func storeTemplateFile(templateFilepath, content string) (bool, error) {
+	// Make sure the directory exists.
+	dir := filepath.Dir(templateFilepath)
 	err := os.MkdirAll(dir, 0755)
 	if err != nil {
 		return false, fmt.Errorf("unable to create Alertmanager templates directory %q: %s", dir, err)
 	}
 
-	file := filepath.Join(dir, fn)
 	// Check if the template file already exists and if it has changed
-	if tmpl, err := ioutil.ReadFile(file); err == nil && string(tmpl) == content {
+	if tmpl, err := ioutil.ReadFile(templateFilepath); err == nil && string(tmpl) == content {
 		return false, nil
+	} else if err != nil && !os.IsNotExist(err) {
+		return false, err
 	}
 
-	if err := ioutil.WriteFile(file, []byte(content), 0644); err != nil {
-		return false, fmt.Errorf("unable to create Alertmanager template file %q: %s", file, err)
+	if err := ioutil.WriteFile(templateFilepath, []byte(content), 0644); err != nil {
+		return false, fmt.Errorf("unable to create Alertmanager template file %q: %s", templateFilepath, err)
 	}
 
 	return true, nil
diff --git a/pkg/alertmanager/multitenant_test.go b/pkg/alertmanager/multitenant_test.go
index c55b5105b3..9719177fad 100644
--- a/pkg/alertmanager/multitenant_test.go
+++ b/pkg/alertmanager/multitenant_test.go
@@ -3,12 +3,14 @@ package alertmanager
 import (
 	"bytes"
 	"context"
+	"errors"
 	"fmt"
 	"io/ioutil"
 	"net/http"
 	"net/http/httptest"
 	"net/http/pprof"
 	"os"
+	"path/filepath"
 	"strings"
 	"testing"
 	"time"
@@ -834,3 +836,54 @@ func TestAlertmanager_InitialSyncFailureWithSharding(t *testing.T) {
 	require.False(t, am.ringLifecycler.IsRegistered())
 	require.NotNil(t, am.ring)
 }
+
+func TestSafeTemplateFilepath(t *testing.T) {
+	tests := map[string]struct {
+		dir          string
+		template     string
+		expectedPath string
+		expectedErr  error
+	}{
+		"should succeed if the provided template is a filename": {
+			dir:          "/data/tenant",
+			template:     "test.tmpl",
+			expectedPath: "/data/tenant/test.tmpl",
+		},
+		"should fail if the provided template is escaping the dir": {
+			dir:         "/data/tenant",
+			template:    "../test.tmpl",
+			expectedErr: errors.New(`invalid template name "../test.tmpl": the template filepath is escaping the per-tenant local directory`),
+		},
+	}
+
+	for testName, testData := range tests {
+		t.Run(testName, func(t *testing.T) {
+			actualPath, actualErr := safeTemplateFilepath(testData.dir, testData.template)
+			assert.Equal(t, testData.expectedErr, actualErr)
+			assert.Equal(t, testData.expectedPath, actualPath)
+		})
+	}
+}
+
+func TestStoreTemplateFile(t *testing.T) {
+	tempDir, err := ioutil.TempDir(os.TempDir(), "alertmanager")
+	require.NoError(t, err)
+
+	t.Cleanup(func() {
+		require.NoError(t, os.RemoveAll(tempDir))
+	})
+
+	testTemplateDir := filepath.Join(tempDir, "templates")
+
+	changed, err := storeTemplateFile(filepath.Join(testTemplateDir, "some-template"), "content")
+	require.NoError(t, err)
+	require.True(t, changed)
+
+	changed, err = storeTemplateFile(filepath.Join(testTemplateDir, "some-template"), "new content")
+	require.NoError(t, err)
+	require.True(t, changed)
+
+	changed, err = storeTemplateFile(filepath.Join(testTemplateDir, "some-template"), "new content") // reusing previous content
+	require.NoError(t, err)
+	require.False(t, changed)
+}

From 06bbda1ec0bd4d5fc0194d3d28ac8c9fce914707 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20=C5=A0tibran=C3=BD?= <peter.stibrany@grafana.com>
Date: Mon, 19 Apr 2021 09:12:28 +0200
Subject: [PATCH 2/3] Prepare release 1.7.1. (#3)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Peter Štibraný <pstibrany@gmail.com>
---
 CHANGELOG.md                                         | 4 ++++
 VERSION                                              | 2 +-
 docs/guides/running-chunks-storage-with-cassandra.md | 4 ++--
 k8s/alertmanager-dep.yaml                            | 2 +-
 k8s/configs-dep.yaml                                 | 2 +-
 k8s/distributor-dep.yaml                             | 2 +-
 k8s/ingester-dep.yaml                                | 2 +-
 k8s/querier-dep.yaml                                 | 2 +-
 k8s/query-frontend-dep.yaml                          | 2 +-
 k8s/ruler-dep.yaml                                   | 2 +-
 k8s/table-manager-dep.yaml                           | 2 +-
 11 files changed, 15 insertions(+), 11 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index fc8dc257ec..b5fa4a95d2 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,10 @@
 
 ## master / unreleased
 
+## 1.7.1 / 2021-04-27
+
+* [CHANGE] Fix for CVE-2021-31232: Local file disclosure vulnerability when `-experimental.alertmanager.enable-api` is used. The HTTP basic auth `password_file` can be used as an attack vector to send any file content via a webhook. The alertmanager templates can be used as an attack vector to send any file content because the alertmanager can load any text file specified in the templates list.
+
 ## 1.7.0
 
 Note the blocks storage compactor runs a migration task at startup in this version, which can take many minutes and use a lot of RAM.
diff --git a/VERSION b/VERSION
index bd8bf882d0..943f9cbc4e 100644
--- a/VERSION
+++ b/VERSION
@@ -1 +1 @@
-1.7.0
+1.7.1
diff --git a/docs/guides/running-chunks-storage-with-cassandra.md b/docs/guides/running-chunks-storage-with-cassandra.md
index dd2ba00c77..81ab15575b 100644
--- a/docs/guides/running-chunks-storage-with-cassandra.md
+++ b/docs/guides/running-chunks-storage-with-cassandra.md
@@ -109,12 +109,12 @@ storage:
 ```
 
 The latest tag is not published for the Cortex docker image. Visit quay.io/repository/cortexproject/cortex
-to find the latest stable version tag and use it in the command below (currently it is `v1.7.0`).
+to find the latest stable version tag and use it in the command below (currently it is `v1.7.1`).
 
 Run Cortex using the latest stable version:
 
 ```
-docker run -d --name=cortex -v $(pwd)/single-process-config.yaml:/etc/single-process-config.yaml -p 9009:9009  quay.io/cortexproject/cortex:v1.7.0 -config.file=/etc/single-process-config.yaml
+docker run -d --name=cortex -v $(pwd)/single-process-config.yaml:/etc/single-process-config.yaml -p 9009:9009  quay.io/cortexproject/cortex:v1.7.1 -config.file=/etc/single-process-config.yaml
 ```
 In case you prefer to run the master version, please follow this [documentation](../getting-started/getting-started-chunks.md) on how to build Cortex from source.
 
diff --git a/k8s/alertmanager-dep.yaml b/k8s/alertmanager-dep.yaml
index 87b2491f56..1197a6be8f 100644
--- a/k8s/alertmanager-dep.yaml
+++ b/k8s/alertmanager-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: alertmanager
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=alertmanager
diff --git a/k8s/configs-dep.yaml b/k8s/configs-dep.yaml
index c9a4250e5c..f60f57cd1d 100644
--- a/k8s/configs-dep.yaml
+++ b/k8s/configs-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: configs
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=configs
diff --git a/k8s/distributor-dep.yaml b/k8s/distributor-dep.yaml
index 8da60de003..7cf7fa482c 100644
--- a/k8s/distributor-dep.yaml
+++ b/k8s/distributor-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: distributor
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=distributor
diff --git a/k8s/ingester-dep.yaml b/k8s/ingester-dep.yaml
index 81410675b1..8a81603633 100644
--- a/k8s/ingester-dep.yaml
+++ b/k8s/ingester-dep.yaml
@@ -37,7 +37,7 @@ spec:
 
       containers:
       - name: ingester
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=ingester
diff --git a/k8s/querier-dep.yaml b/k8s/querier-dep.yaml
index 7bca6e558b..fecc849f19 100644
--- a/k8s/querier-dep.yaml
+++ b/k8s/querier-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: querier
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=querier
diff --git a/k8s/query-frontend-dep.yaml b/k8s/query-frontend-dep.yaml
index ab9bbf61f8..93dc0f1830 100644
--- a/k8s/query-frontend-dep.yaml
+++ b/k8s/query-frontend-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: query-frontend
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=query-frontend
diff --git a/k8s/ruler-dep.yaml b/k8s/ruler-dep.yaml
index 36eb75dcd6..f904097023 100644
--- a/k8s/ruler-dep.yaml
+++ b/k8s/ruler-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: ruler
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=ruler
diff --git a/k8s/table-manager-dep.yaml b/k8s/table-manager-dep.yaml
index 7098f6d7cc..d1549cb71a 100644
--- a/k8s/table-manager-dep.yaml
+++ b/k8s/table-manager-dep.yaml
@@ -15,7 +15,7 @@ spec:
     spec:
       containers:
       - name: table-manager
-        image: quay.io/cortexproject/cortex:v1.7.0
+        image: quay.io/cortexproject/cortex:v1.7.1
         imagePullPolicy: IfNotPresent
         args:
         - -target=table-manager

From a6b0ffcc23469af20b27410e00fda0fa8363491e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Peter=20S=CC=8Ctibrany=CC=81?= <peter.stibrany@grafana.com>
Date: Mon, 19 Apr 2021 09:54:46 +0200
Subject: [PATCH 3/3] Bring back support for Authorization.CredentialsFile.
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

Signed-off-by: Peter Štibraný <peter.stibrany@grafana.com>
---
 pkg/alertmanager/api.go      |  3 +++
 pkg/alertmanager/api_test.go | 35 ++++++++++++++++++++++++++++++++++-
 2 files changed, 37 insertions(+), 1 deletion(-)

diff --git a/pkg/alertmanager/api.go b/pkg/alertmanager/api.go
index 907a7cc888..cefbbbc634 100644
--- a/pkg/alertmanager/api.go
+++ b/pkg/alertmanager/api.go
@@ -296,6 +296,9 @@ func validateReceiverHTTPConfig(cfg commoncfg.HTTPClientConfig) error {
 	if cfg.BasicAuth != nil && cfg.BasicAuth.PasswordFile != "" {
 		return errPasswordFileNotAllowed
 	}
+	if cfg.Authorization != nil && cfg.Authorization.CredentialsFile != "" {
+		return errPasswordFileNotAllowed
+	}
 	if cfg.BearerTokenFile != "" {
 		return errPasswordFileNotAllowed
 	}
diff --git a/pkg/alertmanager/api_test.go b/pkg/alertmanager/api_test.go
index 230f3877fa..86f10c688d 100644
--- a/pkg/alertmanager/api_test.go
+++ b/pkg/alertmanager/api_test.go
@@ -253,6 +253,22 @@ alertmanager_config: |
     http_config:
       bearer_token_file: /secrets
 
+  route:
+    receiver: 'default-receiver'
+  receivers:
+    - name: default-receiver
+`,
+			err: errors.Wrap(errPasswordFileNotAllowed, "error validating Alertmanager config"),
+		},
+		{
+			name: "Should return error if global HTTP credentials_file is set",
+			cfg: `
+alertmanager_config: |
+  global:
+    http_config:
+      authorization:
+        credentials_file: /secrets
+
   route:
     receiver: 'default-receiver'
   receivers:
@@ -288,6 +304,23 @@ alertmanager_config: |
           http_config:
             bearer_token_file: /secrets
 
+  route:
+    receiver: 'default-receiver'
+`,
+			err: errors.Wrap(errPasswordFileNotAllowed, "error validating Alertmanager config"),
+		},
+		{
+			name: "Should return error if receiver's HTTP credentials_file is set",
+			cfg: `
+alertmanager_config: |
+  receivers:
+    - name: default-receiver
+      webhook_configs:
+        - url: http://localhost
+          http_config:
+            authorization:
+              credentials_file: /secrets
+
   route:
     receiver: 'default-receiver'
 `,
@@ -480,7 +513,7 @@ func TestValidateAlertmanagerConfig(t *testing.T) {
 	for testName, testData := range tests {
 		t.Run(testName, func(t *testing.T) {
 			err := validateAlertmanagerConfig(testData.input)
-			assert.True(t, errors.Is(err, testData.expected))
+			assert.ErrorIs(t, err, testData.expected)
 		})
 	}
 }