Information Disclosure/Credential exposure

HIGH
grafana/grafana
Commit: bb674a5354d9
Affected: < 12.4.0 (releases prior to this patch; tracked version 12.4.0 includes the fix)
2026-05-29 17:25 UTC

Description

This commit fixes a credential disclosure risk in Grafana provisioning. Previously, when a provisioning git repository URL used http:// and a token was configured, the token could be sent in cleartext during Git operations because git.NewRepository would attach basic auth unconditionally for HTTP. The validators did not reject this combination. The change adds an allowInsecure flag and updates validation to reject http:// URLs when a token is configured, except when running in development mode (app_mode=development) or when provisioning.allow_insecure is explicitly enabled. It also normalizes the URL scheme to lowercase to catch cases like HTTP://. The behavior for https:// URLs remains unchanged, and http:// is still allowed without a token. This reduces the risk of token leakage over non-TLS transport during provisioning.

Proof of Concept

PoC PoC (proof of concept) to demonstrate credential exposure prior to this fix: 1) Spin up a local HTTP endpoint that logs any Basic Auth credentials sent by clients: // Node.js (server.js) const http = require('http'); http.createServer((req, res) => { const auth = req.headers['authorization']; if (auth) { console.log('Authorization header captured:', auth); } else { console.log('No Authorization header in request'); } res.statusCode = 200; res.end('OK'); }).listen(8080, () => console.log('Listening on 8080')); 2) Run the server: node server.js 3) In a separate terminal, simulate a Git HTTP request that Grafana would perform when a token is configured with an http URL. This uses Basic authentication with the token as the password (username can be arbitrary or a service token user): curl -s -u attacker:mytoken http://localhost:8080/ 4) Observe the server logs contain the Authorization header (Base64-encoded credentials). Decode to reveal the token: # On Linux/macOS echo 'Authorization header value from server' # capture from logs # Example decoding (replace with actual header value): # echo '<base64-credentials>' | base64 -d # Output would include the token, illustrating credential exposure over HTTP. 5) Context: Grafana provisioning would transmit the token as part of Basic auth when using an http URL with a token. An interceptor or man-in-the-middle on an unencrypted network could capture this token and reuse it to access the repository. This PoC uses a minimal HTTP server to show that tokens can be exposed in cleartext over http, illustrating the risk that the patch mitigates by enforcing https or enabling a deliberate insecure mode for dev environments.

Commit Details

Author: Roberto Jiménez Sánchez

Date: 2026-05-29 17:07 UTC

Message:

Provisioning: reject http:// repository URLs when a token is configured (#125732) * Provisioning: reject http:// repository URLs when a token is configured Combining an http:// repository URL with a token caused the token to travel in cleartext on every git operation, since git.NewRepository adds basic auth unconditionally regardless of scheme. The validators never rejected this combination. Reject http:// + token in the shared ValidateGitConfigFields (covering git and github, plus future git-based types), with an escape hatch for local/dev: the combination is still allowed when running in development mode (app_mode=development) or when the new [provisioning] allow_insecure_token flag is set. http:// without a token and https:// remain valid everywhere. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * docs: document [provisioning] allow_insecure_token option Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Provisioning: rename allow_insecure_token to allow_insecure Aligns the config key with the internal allowInsecure naming and follows Grafana's existing convention. The flag still gates only the http:// + token combination. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Provisioning: enable allow_insecure in integration test harness Provisioning integration tests run against a local Gitea container over http:// with a token, which the new validator rejects by default. Add a ProvisioningAllowInsecure option to the test GrafanaOpts and enable it in the default provisioning test config so these tests keep working. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * Provisioning: normalize URL scheme in http+token check URL schemes are case-insensitive, so a value like HTTP:// could bypass the case-sensitive prefix check and still send the token in cleartext. Lowercase the URL before comparing (https:// remains unaffected). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>

Triage Assessment

Vulnerability Type: Information Disclosure/Credential exposure

Confidence: HIGH

Reasoning:

The commit adds validation to reject http:// repository URLs when a token is configured, preventing credentials from traveling in cleartext during git operations. It introduces an allowInsecure/allow_insecure flag to permit this only in development with explicit opt-in, otherwise enforcing https. This directly mitigates an information disclosure risk (cleartext credentials) and related credential exposure in transport.

Verification Assessment

Vulnerability Type: Information Disclosure/Credential exposure

Confidence: HIGH

Affected Versions: < 12.4.0 (releases prior to this patch; tracked version 12.4.0 includes the fix)

Code Diff

diff --git a/apps/provisioning/pkg/repository/git/extra.go b/apps/provisioning/pkg/repository/git/extra.go index 1f9e9fbb40540..4515ff4aa96b6 100644 --- a/apps/provisioning/pkg/repository/git/extra.go +++ b/apps/provisioning/pkg/repository/git/extra.go @@ -12,11 +12,14 @@ import ( type extra struct { decrypter repository.Decrypter + // allowInsecure permits http:// URLs together with a token (cleartext credentials); local/dev only. + allowInsecure bool } -func Extra(decrypter repository.Decrypter) repository.Extra { +func Extra(decrypter repository.Decrypter, allowInsecure bool) repository.Extra { return &extra{ - decrypter: decrypter, + decrypter: decrypter, + allowInsecure: allowInsecure, } } @@ -51,5 +54,5 @@ func (e *extra) Mutate(ctx context.Context, obj runtime.Object) error { } func (e *extra) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return Validate(ctx, obj) + return Validate(ctx, obj, e.allowInsecure) } diff --git a/apps/provisioning/pkg/repository/git/validator.go b/apps/provisioning/pkg/repository/git/validator.go index 8523f841ebca9..f32a9d4ac35ed 100644 --- a/apps/provisioning/pkg/repository/git/validator.go +++ b/apps/provisioning/pkg/repository/git/validator.go @@ -12,7 +12,9 @@ import ( ) // Validate validates the git repository configuration without requiring decrypted secrets. -func Validate(_ context.Context, obj runtime.Object) field.ErrorList { +// allowInsecure permits http:// URLs together with a token (cleartext credentials); it should +// only be true for local/dev environments. +func Validate(_ context.Context, obj runtime.Object, allowInsecure bool) field.ErrorList { repo, ok := obj.(*provisioning.Repository) if !ok { return nil @@ -29,20 +31,22 @@ func Validate(_ context.Context, obj runtime.Object) field.ErrorList { } } - return validateGitConfig(repo, cfg) + return validateGitConfig(repo, cfg, allowInsecure) } // validateGitConfig validates the git configuration fields. // This is extracted to be reusable by other git-based repository types (github, gitlab, bitbucket). -func validateGitConfig(repo *provisioning.Repository, cfg *provisioning.GitRepositoryConfig) field.ErrorList { - return ValidateGitConfigFields(repo, cfg.URL, cfg.Branch, cfg.Path) +func validateGitConfig(repo *provisioning.Repository, cfg *provisioning.GitRepositoryConfig, allowInsecure bool) field.ErrorList { + return ValidateGitConfigFields(repo, cfg.URL, cfg.Branch, cfg.Path, allowInsecure) } // ValidateGitConfigFields validates common git configuration fields (Branch, Path, token/connection). // This can be reused by git-based repository types (github, gitlab, bitbucket). // The URL parameter is only used for token/connection validation logic, not for URL format validation // (providers handle their own URL format validation). -func ValidateGitConfigFields(repo *provisioning.Repository, url, branch, path string) field.ErrorList { +// allowInsecure permits http:// URLs together with a token; when false, that combination is rejected +// because it sends credentials in cleartext. +func ValidateGitConfigFields(repo *provisioning.Repository, url, branch, path string, allowInsecure bool) field.ErrorList { var list field.ErrorList t := string(repo.Spec.Type) @@ -58,6 +62,15 @@ func ValidateGitConfigFields(repo *provisioning.Repository, url, branch, path st } } + // Reject http:// together with a token: the token would travel in cleartext on every git + // operation. The token is a presence check (IsZero) that works without decryption. + // URL schemes are case-insensitive, so normalize before comparing (https:// is unaffected, + // since it does not have the http:// prefix). + if !allowInsecure && strings.HasPrefix(strings.ToLower(url), "http://") && !repo.Secure.Token.IsZero() { + list = append(list, field.Invalid(field.NewPath("spec", t, "url"), url, + "http:// is not allowed when a token is configured; use https:// to avoid sending credentials in cleartext")) + } + // Validate branch name format if a branch is provided (applies to all repository types) if branch != "" && !IsValidGitBranchName(branch) { list = append(list, field.Invalid(field.NewPath("spec", t, "branch"), branch, "invalid branch name")) diff --git a/apps/provisioning/pkg/repository/git/validator_test.go b/apps/provisioning/pkg/repository/git/validator_test.go index 7e576dce11f61..712fcdb40e1df 100644 --- a/apps/provisioning/pkg/repository/git/validator_test.go +++ b/apps/provisioning/pkg/repository/git/validator_test.go @@ -16,6 +16,7 @@ func TestValidate(t *testing.T) { tests := []struct { name string obj runtime.Object + allowInsecure bool expectedError bool errorContains []string }{ @@ -231,6 +232,90 @@ func TestValidate(t *testing.T) { }, }, }, + { + name: "http url with token is rejected", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitRepositoryType, + Git: &provisioning.GitRepositoryConfig{ + URL: "http://localhost:3000/grafana/grafana.git", + Branch: "main", + Path: "grafana", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Create: common.NewSecretValue("test-token"), + }, + }, + }, + expectedError: true, + errorContains: []string{"http:// is not allowed when a token is configured"}, + }, + { + name: "http url with token is allowed when insecure is permitted", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitRepositoryType, + Git: &provisioning.GitRepositoryConfig{ + URL: "http://localhost:3000/grafana/grafana.git", + Branch: "main", + Path: "grafana", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Create: common.NewSecretValue("test-token"), + }, + }, + }, + allowInsecure: true, + }, + { + name: "uppercase HTTP scheme with token is rejected", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitRepositoryType, + Git: &provisioning.GitRepositoryConfig{ + URL: "HTTP://localhost:3000/grafana/grafana.git", + Branch: "main", + Path: "grafana", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Create: common.NewSecretValue("test-token"), + }, + }, + }, + expectedError: true, + errorContains: []string{"http:// is not allowed when a token is configured"}, + }, + { + name: "http url without token is allowed", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitRepositoryType, + Git: &provisioning.GitRepositoryConfig{ + URL: "http://localhost:3000/grafana/grafana.git", + Branch: "main", + Path: "grafana", + }, + }, + }, + }, { name: "valid git repository with connection", obj: &provisioning.Repository{ @@ -255,7 +340,7 @@ func TestValidate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - list := Validate(context.Background(), tt.obj) + list := Validate(context.Background(), tt.obj, tt.allowInsecure) if tt.expectedError { assert.NotEmpty(t, list) if len(tt.errorContains) > 0 { diff --git a/apps/provisioning/pkg/repository/github/extra.go b/apps/provisioning/pkg/repository/github/extra.go index 8bf10b0d5e561..357bdb86fd285 100644 --- a/apps/provisioning/pkg/repository/github/extra.go +++ b/apps/provisioning/pkg/repository/github/extra.go @@ -24,14 +24,17 @@ type extra struct { decrypter repository.Decrypter webhookBuilder WebhookURLBuilder incrementalPolicy repository.IncrementalSyncPolicy + // allowInsecure permits http:// URLs together with a token (cleartext credentials); local/dev only. + allowInsecure bool } -func Extra(decrypter repository.Decrypter, factory *Factory, webhookBuilder WebhookURLBuilder, incrementalPolicy repository.IncrementalSyncPolicy) repository.Extra { +func Extra(decrypter repository.Decrypter, factory *Factory, webhookBuilder WebhookURLBuilder, incrementalPolicy repository.IncrementalSyncPolicy, allowInsecure bool) repository.Extra { return &extra{ decrypter: decrypter, factory: factory, webhookBuilder: webhookBuilder, incrementalPolicy: incrementalPolicy, + allowInsecure: allowInsecure, } } @@ -90,5 +93,5 @@ func (e *extra) Mutate(ctx context.Context, obj runtime.Object) error { } func (e *extra) Validate(ctx context.Context, obj runtime.Object) field.ErrorList { - return Validate(ctx, obj) + return Validate(ctx, obj, e.allowInsecure) } diff --git a/apps/provisioning/pkg/repository/github/extra_test.go b/apps/provisioning/pkg/repository/github/extra_test.go index b03789776df1a..dcdda8f544af0 100644 --- a/apps/provisioning/pkg/repository/github/extra_test.go +++ b/apps/provisioning/pkg/repository/github/extra_test.go @@ -32,7 +32,7 @@ func (m *mockSecureValues) WebhookSecret(_ context.Context) (common.RawSecureVal } func TestExtra_Type(t *testing.T) { - e := github.Extra(nil, nil, nil, repository.IncrementalSyncPolicy{}) + e := github.Extra(nil, nil, nil, repository.IncrementalSyncPolicy{}, false) assert.Equal(t, provisioning.GitHubRepositoryType, e.Type()) } @@ -263,7 +263,7 @@ func TestExtra_Build(t *testing.T) { webhookBuilder := tt.setupWebhook(t, tt.repo) factory := github.ProvideFactory() - e := github.Extra(decrypter, factory, webhookBuilder, repository.IncrementalSyncPolicy{}) + e := github.Extra(decrypter, factory, webhookBuilder, repository.IncrementalSyncPolicy{}, false) result, err := e.Build(ctx, tt.repo) @@ -393,7 +393,7 @@ func TestExtra_Mutate(t *testing.T) { t.Run(tt.name, func(t *testing.T) { ctx := context.Background() - e := github.Extra(nil, nil, nil, repository.IncrementalSyncPolicy{}) + e := github.Extra(nil, nil, nil, repository.IncrementalSyncPolicy{}, false) err := e.Mutate(ctx, tt.obj) diff --git a/apps/provisioning/pkg/repository/github/validator.go b/apps/provisioning/pkg/repository/github/validator.go index 168ab15bf94b9..64a5f3e73f074 100644 --- a/apps/provisioning/pkg/repository/github/validator.go +++ b/apps/provisioning/pkg/repository/github/validator.go @@ -12,7 +12,9 @@ import ( ) // Validate validates the github repository configuration without requiring decrypted secrets. -func Validate(_ context.Context, obj runtime.Object) field.ErrorList { +// allowInsecure permits http:// URLs together with a token (cleartext credentials); it should +// only be true for local/dev environments. +func Validate(_ context.Context, obj runtime.Object, allowInsecure bool) field.ErrorList { repo, ok := obj.(*provisioning.Repository) if !ok { return nil @@ -49,6 +51,6 @@ func Validate(_ context.Context, obj runtime.Object) field.ErrorList { } // Validate git-related fields (branch, path, token/connection) using the shared git validator - list = append(list, git.ValidateGitConfigFields(repo, gh.URL, gh.Branch, gh.Path)...) + list = append(list, git.ValidateGitConfigFields(repo, gh.URL, gh.Branch, gh.Path, allowInsecure)...) return list } diff --git a/apps/provisioning/pkg/repository/github/validator_test.go b/apps/provisioning/pkg/repository/github/validator_test.go index c2c6eaedf44ef..a553b3fefd099 100644 --- a/apps/provisioning/pkg/repository/github/validator_test.go +++ b/apps/provisioning/pkg/repository/github/validator_test.go @@ -16,6 +16,7 @@ func TestValidate(t *testing.T) { tests := []struct { name string obj runtime.Object + allowInsecure bool expectedError bool errorContains []string }{ @@ -65,7 +66,7 @@ func TestValidate(t *testing.T) { errorContains: []string{"url"}, }, { - name: "valid HTTP URL for local development", + name: "http URL with token is rejected by default", obj: &provisioning.Repository{ ObjectMeta: metav1.ObjectMeta{ Name: "test-repo", @@ -83,7 +84,44 @@ func TestValidate(t *testing.T) { }, }, }, - expectedError: false, + expectedError: true, + errorContains: []string{"http:// is not allowed when a token is configured"}, + }, + { + name: "http URL with token is allowed when insecure is permitted (local development)", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitHubRepositoryType, + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "http://github.com/grafana/grafana", + Branch: "main", + }, + }, + Secure: provisioning.SecureValues{ + Token: common.InlineSecureValue{ + Create: common.NewSecretValue("test-token"), + }, + }, + }, + allowInsecure: true, + }, + { + name: "http URL without token is allowed", + obj: &provisioning.Repository{ + ObjectMeta: metav1.ObjectMeta{ + Name: "test-repo", + }, + Spec: provisioning.RepositorySpec{ + Type: provisioning.GitHubRepositoryType, + GitHub: &provisioning.GitHubRepositoryConfig{ + URL: "http://github.com/grafana/grafana", + Branch: "main", + }, + }, + }, }, { name: "valid github.com repository", @@ -110,7 +148,7 @@ func TestValidate(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - list := Validate(context.Background(), tt.obj) + list := Validate(context.Background(), tt.obj, tt.allowInsecure) if tt.expectedError { assert.NotEmpty(t, list) if len(tt.errorContains) > 0 { diff --git a/conf/defaults.ini b/conf/defaults.ini index 16cd4842161e9..cefd01bc79346 100644 --- a/conf/defaults.ini +++ b/conf/defaults.ini @@ -2405,6 +2405,11 @@ allowed_targets = folder # Requires image rendering service to be configured. allow_image_rendering = true +# Allow http:// repository URLs together with a configured token. This sends the token in +# cleartext on every git operation, so it is rejected by default. Intended for local/dev only +# (it is also implicitly allowed when app_mode = development). +allow_insecure = false + # The minimum sync interval that can be set for a repository. This is how often the controller # will check if there has been any changes to the repository not propagated by a webhook. # The minimum value is 10 seconds. diff --git a/pkg/operators/provisioning/config.go b/pkg/operators/provisioning/config.go index 33acdf1f40959..afecfac39e21a 100644 --- a/pkg/operators/provisioning/config.go +++ b/p ... [truncated]
← Back to Alerts View on GitHub →