Input validation / Authorization bypass

HIGH
grafana/grafana
Commit: efe853c7f079
Affected: <= 12.3.x (Grafana versions prior to this patch); 12.4.0 includes the fix
2026-05-08 10:40 UTC

Description

The commit adds explicit repository-origin validation for GitHub webhook events in the provisioning subsystem. Previously, webhook events (e.g., push or pull_request) could be processed without verifying that the event's repository matched the configured Grafana provisioning repository (owner/repo). The fix introduces a repository mismatch check and returns a BadRequest with a repository mismatch error when the event's repository does not match the expected one. It also logs warnings when mismatches occur and propagates a dedicated ErrRepositoryMismatch error. This reduces the risk of unauthorized provisioning actions triggered by spoofed or misrouted webhook payloads.

Proof of Concept

PoC (proof-of-concept) to reproduce the vulnerability prior to the fix: 1) Assumptions: You control a GitHub repository and Grafana is configured to receive GitHub webhooks for provisioning, with a known webhook secret (used to compute the X-Hub-Signature-256). The Grafana instance is configured to provision resources for owner/repo = grafana/grafana. 2) Build a webhook payload that mimics a GitHub push event from a different repository, e.g., repository.full_name = evil/repo. The rest of the payload can be a minimal valid push event payload. 3) Compute a valid HMAC-SHA256 signature over the payload using the known webhook secret and set the header X-Hub-Signature-256: sha256=<signature>. 4) Send the webhook to Grafana's provisioning endpoint (e.g., POST https://grafana.example.com/api/provisioning/github/webhook) with headers: Content-Type: application/json, X-GitHub-Event: push, X-Hub-Signature-256: sha256=<signature>. 5) Observe that the request is accepted by the webhook handler only if the repository matches Grafana's configured owner/repo; otherwise, the server should reject with a 400 Bad Request indicating repository mismatch. 6) Expected outcome after the fix: Grafana responds with a 400 Bad Request and a repository mismatch error when the payload contains a non-matching repository. Prerequisites: access to Grafana webhook secret, a reachable provisioning webhook endpoint, and a payload containing a mismatched repository full_name. Code example (Python to generate signature and curl payload): import json import hmac import hashlib import requests secret = b'YOUR_WEBHOOK_SECRET' # replace with actual Grafana webhook secret payload = { "repository": {"full_name": "evil/repo"}, "ref": "refs/heads/main", "after": "0000000000000000000000000000000000000000" } body = json.dumps(payload, separators=(",", ":")) signature = 'sha256=' + hmac.new(secret, body.encode(), hashlib.sha256).hexdigest() url = 'https://grafana.example.com/api/provisioning/github/webhook' # endpoint used for provisioning headers = { 'Content-Type': 'application/json', 'X-GitHub-Event': 'push', 'X-Hub-Signature-256': signature } resp = requests.post(url, data=body, headers=headers, verify=False) print(resp.status_code, resp.text) Notes: - If the vulnerability is present (pre-fix), Grafana may process the event for the Grafana repo despite the mismatched repository field, potentially triggering provisioning actions. After the fix, the endpoint should reject with repository mismatch (HTTP 400) when the repository does not match the configured owner/repo.

Commit Details

Author: Daniele Stefano Ferru

Date: 2026-05-08 10:04 UTC

Message:

Provisioning: Return Bad request for repo mismatch in webhook (#124453) * Provisioning: Return Bad request for repo mismatch in webhook * linting * addressing comment

Triage Assessment

Vulnerability Type: Input validation / Authorization bypass

Confidence: HIGH

Reasoning:

The commit adds explicit validation to ensure webhook events originate from the expected repository and returns a BadRequest / repository mismatch error when they don't. This prevents processing of webhooks from unintended repos, reducing the risk of unauthorized actions triggered by spoofed or mismatched webhook payloads.

Verification Assessment

Vulnerability Type: Input validation / Authorization bypass

Confidence: HIGH

Affected Versions: <= 12.3.x (Grafana versions prior to this patch); 12.4.0 includes the fix

Code Diff

diff --git a/apps/provisioning/pkg/repository/github/webhook.go b/apps/provisioning/pkg/repository/github/webhook.go index cbb1a56fe3e70..cb953c22be3e7 100644 --- a/apps/provisioning/pkg/repository/github/webhook.go +++ b/apps/provisioning/pkg/repository/github/webhook.go @@ -76,11 +76,11 @@ func (r *githubWebhookRepository) Webhook(ctx context.Context, req *http.Request return nil, apierrors.NewUnauthorized("invalid signature") } - return r.parseWebhook(github.WebHookType(req), payload) + return r.parseWebhook(ctx, github.WebHookType(req), payload) } // This method does not include context because it does delegate any more requests -func (r *githubWebhookRepository) parseWebhook(messageType string, payload []byte) (*provisioning.WebhookResponse, error) { +func (r *githubWebhookRepository) parseWebhook(ctx context.Context, messageType string, payload []byte) (*provisioning.WebhookResponse, error) { event, err := github.ParseWebHook(messageType, payload) if err != nil { return nil, apierrors.NewBadRequest("invalid payload") @@ -88,7 +88,7 @@ func (r *githubWebhookRepository) parseWebhook(messageType string, payload []byt switch event := event.(type) { case *github.PushEvent: - return r.parsePushEvent(event) + return r.parsePushEvent(ctx, event) case *github.PullRequestEvent: return r.parsePullRequestEvent(event) case *github.PingEvent: @@ -104,12 +104,16 @@ func (r *githubWebhookRepository) parseWebhook(messageType string, payload []byt } } -func (r *githubWebhookRepository) parsePushEvent(event *github.PushEvent) (*provisioning.WebhookResponse, error) { +func (r *githubWebhookRepository) parsePushEvent(ctx context.Context, event *github.PushEvent) (*provisioning.WebhookResponse, error) { + _, logger := r.logger(ctx, "") + if event.GetRepo() == nil { return nil, fmt.Errorf("missing repository in push event") } - if event.GetRepo().GetFullName() != fmt.Sprintf("%s/%s", r.owner, r.repo) { - return nil, fmt.Errorf("repository mismatch") + expected := fmt.Sprintf("%s/%s", r.owner, r.repo) + if event.GetRepo().GetFullName() != expected { + logger.Warn("webhook push event repository mismatch", "expected", expected, "got", event.GetRepo().GetFullName()) + return nil, repository.ErrRepositoryMismatch } // No need to sync if not enabled @@ -153,8 +157,10 @@ func (r *githubWebhookRepository) parsePullRequestEvent(event *github.PullReques return nil, fmt.Errorf("missing GitHub config") } - if event.GetRepo().GetFullName() != fmt.Sprintf("%s/%s", r.owner, r.repo) { - return nil, fmt.Errorf("repository mismatch") + expected := fmt.Sprintf("%s/%s", r.owner, r.repo) + if event.GetRepo().GetFullName() != expected { + slog.Warn("webhook pull request event repository mismatch", "expected", expected, "got", event.GetRepo().GetFullName()) + return nil, repository.ErrRepositoryMismatch } pr := event.GetPullRequest() if pr == nil { diff --git a/apps/provisioning/pkg/repository/github/webhook_test.go b/apps/provisioning/pkg/repository/github/webhook_test.go index ea682d20747ba..b12fe8ba3e2a8 100644 --- a/apps/provisioning/pkg/repository/github/webhook_test.go +++ b/apps/provisioning/pkg/repository/github/webhook_test.go @@ -133,7 +133,7 @@ func TestParseWebhooks(t *testing.T) { payload, err := os.ReadFile(path.Join("testdata", name)) require.NoError(t, err) - rsp, err := gh.parseWebhook(tt.messageType, payload) + rsp, err := gh.parseWebhook(context.Background(), tt.messageType, payload) require.NoError(t, err) require.Equal(t, tt.expected.Code, rsp.Code) @@ -167,7 +167,7 @@ func TestParsePushEvent_LargeDiffForcesFullSync(t *testing.T) { payload, err := os.ReadFile(path.Join("testdata", "webhook-push-large_diff.json")) require.NoError(t, err) - rsp, err := gh.parseWebhook("push", payload) + rsp, err := gh.parseWebhook(context.Background(), "push", payload) require.NoError(t, err) require.Equal(t, http.StatusAccepted, rsp.Code) @@ -400,7 +400,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - expectedError: fmt.Errorf("repository mismatch"), + expectedError: repo.ErrRepositoryMismatch, }, { name: "push event when sync is disabled", @@ -779,7 +779,7 @@ func TestGitHubRepository_Webhook(t *testing.T) { return req }, - expectedError: fmt.Errorf("repository mismatch"), + expectedError: repo.ErrRepositoryMismatch, }, { name: "pull request event missing pull request info", diff --git a/apps/provisioning/pkg/repository/repository.go b/apps/provisioning/pkg/repository/repository.go index 73c16d48e9b1a..4020984fece93 100644 --- a/apps/provisioning/pkg/repository/repository.go +++ b/apps/provisioning/pkg/repository/repository.go @@ -76,6 +76,8 @@ var ErrTooManyItems error = &apierrors.StatusError{ErrStatus: metav1.Status{ Message: "maximum number of items exceeded", }} +var ErrRepositoryMismatch = apierrors.NewBadRequest("repository mismatch") + type FileInfo struct { // Path to the file on disk. // No leading or trailing slashes will be contained within.
← Back to Alerts View on GitHub →