From bf1584cc3a6363bad39c33f7f2e465acd85a3d0c Mon Sep 17 00:00:00 2001 From: Friedrich Gonzalez <1517449+friedrichg@users.noreply.github.com> Date: Fri, 14 Aug 2026 11:29:03 -0700 Subject: [PATCH 1/2] Alertmanager: reject file-based http_headers in tenant configs Signed-off-by: Friedrich Gonzalez <1517449+friedrichg@users.noreply.github.com> --- CHANGELOG.md | 1 + pkg/alertmanager/api.go | 27 ++++++++ pkg/alertmanager/api_test.go | 131 +++++++++++++++++++++++++++++++++++ 3 files changed, 159 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index abd8bcc792..25b7a15112 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,6 +88,7 @@ * [BUGFIX] Compactor: Fix spurious `bucket operation fail after retries` error logs emitted during partial block cleanup. #7749 * [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: Tighten per-tenant config validation to reject additional file-based settings. ## 1.21.1 2026-06-04 diff --git a/pkg/alertmanager/api.go b/pkg/alertmanager/api.go index 3a9a604996..7ca31aff8a 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 1a007819b0..14e56d9722 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: ` @@ -1358,6 +1420,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, + }, } for testName, testData := range tests { From 7b493c1c1c38d00491fa0056bdd7e8caa493dc37 Mon Sep 17 00:00:00 2001 From: Friedrich Gonzalez <1517449+friedrichg@users.noreply.github.com> Date: Fri, 14 Aug 2026 15:01:40 -0700 Subject: [PATCH 2/2] Add PR number to CHANGELOG entry Signed-off-by: Friedrich Gonzalez <1517449+friedrichg@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 25b7a15112..0201037cbd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -88,7 +88,7 @@ * [BUGFIX] Compactor: Fix spurious `bucket operation fail after retries` error logs emitted during partial block cleanup. #7749 * [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: Tighten per-tenant config validation to reject additional file-based settings. +* [BUGFIX] Alertmanager: Tighten per-tenant config validation to reject additional file-based settings. #7767 ## 1.21.1 2026-06-04