Skip to content

Commit 5fc0350

Browse files
authored
Fix for CVE-2021-31232: Alertmanager can expose local files content via specially crafted config (#4129)
* Improve alertmanager template checks Added HTTP and TLS validation Use reflection to scan alertmanager config for validation Do not allow SlackAPIURLFile, APIURLFile, APIKeyFile and ProxyURL too Signed-off-by: Marco Pracucci <marco@pracucci.com> * Added CHANGELOG entry Signed-off-by: Marco Pracucci <marco@pracucci.com>
1 parent fbde82d commit 5fc0350

File tree

6 files changed

+665
-61
lines changed

6 files changed

+665
-61
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
## master / unreleased
44

5+
* [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. #4129
56
* [CHANGE] Alertmanager now removes local files after Alertmanager is no longer running for removed or resharded user. #3910
67
* [CHANGE] Alertmanager now stores local files in per-tenant folders. Files stored by Alertmanager previously are migrated to new hierarchy. Support for this migration will be removed in Cortex 1.11. #3910
78
* [CHANGE] Ruler: deprecated `-ruler.storage.*` CLI flags (and their respective YAML config options) in favour of `-ruler-storage.*`. The deprecated config will be removed in Cortex 1.11. #3945

pkg/alertmanager/alertmanager.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -288,10 +288,13 @@ func clusterWait(position func() int, timeout time.Duration) func() time.Duratio
288288
// ApplyConfig applies a new configuration to an Alertmanager.
289289
func (am *Alertmanager) ApplyConfig(userID string, conf *config.Config, rawCfg string) error {
290290
templateFiles := make([]string, len(conf.Templates))
291-
if len(conf.Templates) > 0 {
292-
for i, t := range conf.Templates {
293-
templateFiles[i] = filepath.Join(am.cfg.TenantDataDir, templatesDir, t)
291+
for i, t := range conf.Templates {
292+
templateFilepath, err := safeTemplateFilepath(filepath.Join(am.cfg.TenantDataDir, templatesDir), t)
293+
if err != nil {
294+
return err
294295
}
296+
297+
templateFiles[i] = templateFilepath
295298
}
296299

297300
tmpl, err := template.FromGlobs(templateFiles...)

pkg/alertmanager/api.go

Lines changed: 184 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,7 @@ import (
77
"net/http"
88
"os"
99
"path/filepath"
10-
11-
"github.com/pkg/errors"
10+
"reflect"
1211

1312
"github.com/cortexproject/cortex/pkg/alertmanager/alertspb"
1413
"github.com/cortexproject/cortex/pkg/tenant"
@@ -18,8 +17,10 @@ import (
1817

1918
"github.com/go-kit/kit/log"
2019
"github.com/go-kit/kit/log/level"
20+
"github.com/pkg/errors"
2121
"github.com/prometheus/alertmanager/config"
2222
"github.com/prometheus/alertmanager/template"
23+
commoncfg "github.com/prometheus/common/config"
2324
"gopkg.in/yaml.v2"
2425
)
2526

@@ -35,6 +36,14 @@ const (
3536
fetchConcurrency = 16
3637
)
3738

39+
var (
40+
errPasswordFileNotAllowed = errors.New("setting password_file, bearer_token_file and credentials_file is not allowed")
41+
errProxyURLNotAllowed = errors.New("setting proxy_url is not allowed")
42+
errTLSFileNotAllowed = errors.New("setting TLS ca_file, cert_file and key_file is not allowed")
43+
errSlackAPIURLFileNotAllowed = errors.New("setting Slack api_url_file and global slack_api_url_file is not allowed")
44+
errVictorOpsAPIKeyFileNotAllowed = errors.New("setting VictorOps api_key_file is not allowed")
45+
)
46+
3847
// UserConfig is used to communicate a users alertmanager configs
3948
type UserConfig struct {
4049
TemplateFiles map[string]string `yaml:"template_files"`
@@ -156,6 +165,25 @@ func validateUserConfig(logger log.Logger, cfg alertspb.AlertConfigDesc) error {
156165
return err
157166
}
158167

168+
// Validate the config recursively scanning it.
169+
if err := validateAlertmanagerConfig(amCfg); err != nil {
170+
return err
171+
}
172+
173+
// Validate templates referenced in the alertmanager config.
174+
for _, name := range amCfg.Templates {
175+
if err := validateTemplateFilename(name); err != nil {
176+
return err
177+
}
178+
}
179+
180+
// Validate template files.
181+
for _, tmpl := range cfg.Templates {
182+
if err := validateTemplateFilename(tmpl.Filename); err != nil {
183+
return err
184+
}
185+
}
186+
159187
// Create templates on disk in a temporary directory.
160188
// Note: This means the validation will succeed if we can write to tmp but
161189
// not to configured data dir, and on the flipside, it'll fail if we can't write
@@ -168,10 +196,15 @@ func validateUserConfig(logger log.Logger, cfg alertspb.AlertConfigDesc) error {
168196
defer os.RemoveAll(userTempDir)
169197

170198
for _, tmpl := range cfg.Templates {
171-
_, err := storeTemplateFile(userTempDir, tmpl.Filename, tmpl.Body)
199+
templateFilepath, err := safeTemplateFilepath(userTempDir, tmpl.Filename)
172200
if err != nil {
173-
level.Error(logger).Log("msg", "unable to create template file", "err", err, "user", cfg.User)
174-
return fmt.Errorf("unable to create template file '%s'", tmpl.Filename)
201+
level.Error(logger).Log("msg", "unable to create template file path", "err", err, "user", cfg.User)
202+
return err
203+
}
204+
205+
if _, err = storeTemplateFile(templateFilepath, tmpl.Body); err != nil {
206+
level.Error(logger).Log("msg", "unable to store template file", "err", err, "user", cfg.User)
207+
return fmt.Errorf("unable to store template file '%s'", tmpl.Filename)
175208
}
176209
}
177210

@@ -237,3 +270,149 @@ func (am *MultitenantAlertmanager) ListAllConfigs(w http.ResponseWriter, r *http
237270
close(iter)
238271
<-done
239272
}
273+
274+
// validateAlertmanagerConfig recursively scans the input config looking for data types for which
275+
// we have a specific validation and, whenever encountered, it runs their validation. Returns the
276+
// first error or nil if validation succeeds.
277+
func validateAlertmanagerConfig(cfg interface{}) error {
278+
v := reflect.ValueOf(cfg)
279+
t := v.Type()
280+
281+
// Skip invalid, the zero value or a nil pointer (checked by zero value).
282+
if !v.IsValid() || v.IsZero() {
283+
return nil
284+
}
285+
286+
// If the input config is a pointer then we need to get its value.
287+
// At this point the pointer value can't be nil.
288+
if v.Kind() == reflect.Ptr {
289+
v = v.Elem()
290+
t = v.Type()
291+
}
292+
293+
// Check if the input config is a data type for which we have a specific validation.
294+
// At this point the value can't be a pointer anymore.
295+
switch t {
296+
case reflect.TypeOf(config.GlobalConfig{}):
297+
if err := validateGlobalConfig(v.Interface().(config.GlobalConfig)); err != nil {
298+
return err
299+
}
300+
301+
case reflect.TypeOf(commoncfg.HTTPClientConfig{}):
302+
if err := validateReceiverHTTPConfig(v.Interface().(commoncfg.HTTPClientConfig)); err != nil {
303+
return err
304+
}
305+
306+
case reflect.TypeOf(commoncfg.TLSConfig{}):
307+
if err := validateReceiverTLSConfig(v.Interface().(commoncfg.TLSConfig)); err != nil {
308+
return err
309+
}
310+
311+
case reflect.TypeOf(config.SlackConfig{}):
312+
if err := validateSlackConfig(v.Interface().(config.SlackConfig)); err != nil {
313+
return err
314+
}
315+
316+
case reflect.TypeOf(config.VictorOpsConfig{}):
317+
if err := validateVictorOpsConfig(v.Interface().(config.VictorOpsConfig)); err != nil {
318+
return err
319+
}
320+
}
321+
322+
// If the input config is a struct, recursively iterate on all fields.
323+
if t.Kind() == reflect.Struct {
324+
for i := 0; i < t.NumField(); i++ {
325+
field := t.Field(i)
326+
fieldValue := v.FieldByIndex(field.Index)
327+
328+
// Skip any field value which can't be converted to interface (eg. primitive types).
329+
if fieldValue.CanInterface() {
330+
if err := validateAlertmanagerConfig(fieldValue.Interface()); err != nil {
331+
return err
332+
}
333+
}
334+
}
335+
}
336+
337+
if t.Kind() == reflect.Slice || t.Kind() == reflect.Array {
338+
for i := 0; i < v.Len(); i++ {
339+
fieldValue := v.Index(i)
340+
341+
// Skip any field value which can't be converted to interface (eg. primitive types).
342+
if fieldValue.CanInterface() {
343+
if err := validateAlertmanagerConfig(fieldValue.Interface()); err != nil {
344+
return err
345+
}
346+
}
347+
}
348+
}
349+
350+
if t.Kind() == reflect.Map {
351+
for _, key := range v.MapKeys() {
352+
fieldValue := v.MapIndex(key)
353+
354+
// Skip any field value which can't be converted to interface (eg. primitive types).
355+
if fieldValue.CanInterface() {
356+
if err := validateAlertmanagerConfig(fieldValue.Interface()); err != nil {
357+
return err
358+
}
359+
}
360+
}
361+
}
362+
363+
return nil
364+
}
365+
366+
// validateReceiverHTTPConfig validates the HTTP config and returns an error if it contains
367+
// settings not allowed by Cortex.
368+
func validateReceiverHTTPConfig(cfg commoncfg.HTTPClientConfig) error {
369+
if cfg.BasicAuth != nil && cfg.BasicAuth.PasswordFile != "" {
370+
return errPasswordFileNotAllowed
371+
}
372+
if cfg.Authorization != nil && cfg.Authorization.CredentialsFile != "" {
373+
return errPasswordFileNotAllowed
374+
}
375+
if cfg.BearerTokenFile != "" {
376+
return errPasswordFileNotAllowed
377+
}
378+
if cfg.ProxyURL.URL != nil {
379+
return errProxyURLNotAllowed
380+
}
381+
return validateReceiverTLSConfig(cfg.TLSConfig)
382+
}
383+
384+
// validateReceiverTLSConfig validates the TLS config and returns an error if it contains
385+
// settings not allowed by Cortex.
386+
func validateReceiverTLSConfig(cfg commoncfg.TLSConfig) error {
387+
if cfg.CAFile != "" || cfg.CertFile != "" || cfg.KeyFile != "" {
388+
return errTLSFileNotAllowed
389+
}
390+
return nil
391+
}
392+
393+
// validateGlobalConfig validates the Global config and returns an error if it contains
394+
// settings now allowed by Cortex.
395+
func validateGlobalConfig(cfg config.GlobalConfig) error {
396+
if cfg.SlackAPIURLFile != "" {
397+
return errSlackAPIURLFileNotAllowed
398+
}
399+
return nil
400+
}
401+
402+
// validateSlackConfig validates the Slack config and returns an error if it contains
403+
// settings now allowed by Cortex.
404+
func validateSlackConfig(cfg config.SlackConfig) error {
405+
if cfg.APIURLFile != "" {
406+
return errSlackAPIURLFileNotAllowed
407+
}
408+
return nil
409+
}
410+
411+
// validateVictorOpsConfig validates the VictorOps config and returns an error if it contains
412+
// settings now allowed by Cortex.
413+
func validateVictorOpsConfig(cfg config.VictorOpsConfig) error {
414+
if cfg.APIKeyFile != "" {
415+
return errVictorOpsAPIKeyFileNotAllowed
416+
}
417+
return nil
418+
}

0 commit comments

Comments
 (0)