diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d330b9bca..4fe16f3a8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -90,6 +90,7 @@ * [BUGFIX] Alertmanager: Fix panic in `validateAlertmanagerConfig` when receiver config traversal encounters nil interface values. #7751 * [BUGFIX] Parquet Converter: Fix `auto_forget_delay` having no effect. The ring lifecycler was created without the auto-forget delegate, so unhealthy instances were never automatically removed from the ring. #7752 * [BUGFIX] Alertmanager: Reject the global `mattermost_webhook_url_file` setting in per-tenant configs, consistent with every other global `*_file` setting. #7768 +* [BUGFIX] Alertmanager: Tighten per-tenant config validation to reject additional file-based settings. #7767 ## 1.21.1 2026-06-04 diff --git a/pkg/alertmanager/api.go b/pkg/alertmanager/api.go index ec6b2061b3..5739d3e972 100644 --- a/pkg/alertmanager/api.go +++ b/pkg/alertmanager/api.go @@ -58,6 +58,7 @@ var ( errOAuth2CertificateKeyFileNotAllowed = errors.New("setting OAuth2 client_certificate_key_file is not allowed") errOAuth2SecretFileNotAllowed = errors.New("setting OAuth2 client_secret_file is not allowed") errTLSFileNotAllowed = errors.New("setting TLS ca_file, cert_file and key_file is not allowed") + errHTTPHeadersFilesNotAllowed = errors.New("setting http_headers files is not allowed") errSlackAPIURLFileNotAllowed = errors.New("setting Slack api_url_file and global slack_api_url_file is not allowed") errSlackAppTokenFileNotAllowed = errors.New("setting Slack slack_app_token_file and global slack_app_token_file is not allowed") errVictorOpsAPIKeyFileNotAllowed = errors.New("setting VictorOps api_key_file and global victorops_api_key_file is not allowed") @@ -475,9 +476,35 @@ func validateReceiverHTTPConfig(cfg commoncfg.HTTPClientConfig) error { if cfg.OAuth2 != nil && cfg.OAuth2.ClientSecretFile != "" { return errOAuth2SecretFileNotAllowed } + if err := validateReceiverHTTPHeaders(cfg.HTTPHeaders); err != nil { + return err + } return validateReceiverTLSConfig(cfg.TLSConfig) } +// validateReceiverHTTPHeaders validates the configured HTTP headers and returns an error +// if any of them sources its value from a file on the Alertmanager host. +// +// commoncfg.Header.Files is a list of paths that headersRoundTripper.RoundTrip os.ReadFile()s +// at notification time, injecting the contents into an outbound request whose URL the tenant +// also controls. That is the same "tenant config reads a host file" primitive the rest of the +// *_file denylist in this file exists to block, originally added for CVE-2021-31232 (#4129) +// and extended per-receiver for CVE-2022-23536, so it has to be blocked here too. +// +// Only Files is rejected. Values and Secrets are literals supplied inline by the tenant; they +// read nothing from the host and remain allowed, so ordinary header use keeps working. +func validateReceiverHTTPHeaders(headers *commoncfg.Headers) error { + if headers == nil { + return nil + } + for _, header := range headers.Headers { + if len(header.Files) > 0 { + return errHTTPHeadersFilesNotAllowed + } + } + return nil +} + // validateReceiverTLSConfig validates the TLS config and returns an error if it contains // settings not allowed by Cortex. func validateReceiverTLSConfig(cfg commoncfg.TLSConfig) error { diff --git a/pkg/alertmanager/api_test.go b/pkg/alertmanager/api_test.go index 9ed2beebf3..c6d9e82d8c 100644 --- a/pkg/alertmanager/api_test.go +++ b/pkg/alertmanager/api_test.go @@ -462,6 +462,68 @@ alertmanager_config: | `, err: errors.Wrap(errTLSFileNotAllowed, "error validating Alertmanager config"), }, + { + name: "Should return error if receiver's http_headers files is set", + cfg: ` +alertmanager_config: | + receivers: + - name: default-receiver + webhook_configs: + - url: http://localhost + http_config: + http_headers: + X-Canary: + files: + - /var/run/secrets/kubernetes.io/serviceaccount/token + + route: + receiver: 'default-receiver' +`, + err: errors.Wrap(errHTTPHeadersFilesNotAllowed, "error validating Alertmanager config"), + }, + { + name: "Should return error if global http_headers files is set", + cfg: ` +alertmanager_config: | + global: + http_config: + http_headers: + X-Canary: + files: + - /secrets + + receivers: + - name: default-receiver + webhook_configs: + - url: http://localhost + + route: + receiver: 'default-receiver' +`, + err: errors.Wrap(errHTTPHeadersFilesNotAllowed, "error validating Alertmanager config"), + }, + { + name: "Should pass if receiver's http_headers only uses values and secrets", + cfg: ` +alertmanager_config: | + receivers: + - name: default-receiver + webhook_configs: + - url: http://localhost + http_config: + http_headers: + X-Canary: + values: + - canary + X-Token: + secrets: + - sekret + + route: + receiver: 'default-receiver' +`, + err: nil, + }, { name: "Should return error if global opsgenie_api_key_file is set", cfg: ` @@ -1376,6 +1438,75 @@ func TestValidateAlertmanagerConfig(t *testing.T) { }, expected: errTLSFileNotAllowed, }, + "*HTTPClientConfig with http_headers files": { + input: &commoncfg.HTTPClientConfig{ + HTTPHeaders: &commoncfg.Headers{ + Headers: map[string]commoncfg.Header{ + "X-Canary": {Files: []string{"/secrets"}}, + }, + }, + }, + expected: errHTTPHeadersFilesNotAllowed, + }, + "struct containing *HTTPClientConfig with http_headers files as direct child": { + input: config.GlobalConfig{ + HTTPConfig: &commoncfg.HTTPClientConfig{ + HTTPHeaders: &commoncfg.Headers{ + Headers: map[string]commoncfg.Header{ + "X-Canary": {Files: []string{"/secrets"}}, + }, + }, + }, + }, + expected: errHTTPHeadersFilesNotAllowed, + }, + "struct containing *HTTPClientConfig with http_headers files as nested child within a slice": { + input: config.Config{ + Receivers: []config.Receiver{{ + Name: "test", + WebhookConfigs: []*webhook.WebhookConfig{{ + HTTPConfig: &commoncfg.HTTPClientConfig{ + HTTPHeaders: &commoncfg.Headers{ + Headers: map[string]commoncfg.Header{ + "X-Canary": {Files: []string{"/secrets"}}, + }, + }, + }, + }}}, + }, + }, + expected: errHTTPHeadersFilesNotAllowed, + }, + "*HTTPClientConfig with http_headers values only": { + input: &commoncfg.HTTPClientConfig{ + HTTPHeaders: &commoncfg.Headers{ + Headers: map[string]commoncfg.Header{ + "X-Canary": {Values: []string{"value"}}, + }, + }, + }, + expected: nil, + }, + "*HTTPClientConfig with http_headers secrets only": { + input: &commoncfg.HTTPClientConfig{ + HTTPHeaders: &commoncfg.Headers{ + Headers: map[string]commoncfg.Header{ + "X-Canary": {Secrets: []commoncfg.Secret{"secret"}}, + }, + }, + }, + expected: nil, + }, + "*HTTPClientConfig with http_headers empty files": { + input: &commoncfg.HTTPClientConfig{ + HTTPHeaders: &commoncfg.Headers{ + Headers: map[string]commoncfg.Header{ + "X-Canary": {Values: []string{"value"}, Files: []string{}}, + }, + }, + }, + expected: nil, + }, "GlobalConfig with mattermost_webhook_url_file": { input: config.GlobalConfig{ MattermostWebhookURLFile: "/secrets",