Skip to content

Latest commit

 

History

History

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

README.md

CVE-2026-93539 — Unauthenticated GitRepo spec mutation via the Fleet git webhook receiver

  • Advisory: GHSA-8vfv-33cg-g75q · CVE-2026-93539
  • Severity: Medium (CVSS 4.0 6.3 — AV:N/AC:L/AT:P/PR:N/UI:N/VC:N/VI:L/VA:L/SC:N/SI:L/SA:L) · CWE-306 / CWE-862
  • Affected: Fleet >=v0.16.0 <v0.16.2 (shipped with Rancher v2.15.0 and v2.15.1). Earlier Fleet release lines do not perform this spec mutation and were verified as not vulnerable.
  • Patched: Fleet v0.16.2 · Rancher v2.15.2
  • Status: publicly disclosed and fixed. Reported by Pig-Tail through coordinated disclosure.

Summary

Webhook authentication in the gitjob webhook receiver is optional and is silently skipped when no secret exists. Webhook.ServeHTTP first calls parseWebhook(r, nil) to discover the target GitRepo without verifying any signature; the verifying parse runs only if secret != nil. getSecret returns (nil, nil) when neither a per-GitRepo spec.webhookSecret nor the global gitjob-webhook Secret exists, so the signature block is skipped entirely and a fully forged payload is accepted.

That is the out-of-the-box state: the Fleet Helm chart templates no gitjob-webhook Secret, and GitRepo.spec.webhookSecret is empty unless explicitly set.

The handler then performs a privileged, cluster-wide action. It Lists every GitRepo in every namespace (labels.Everything()) and, for each whose spec.repo matches the attacker-supplied repository URL and whose branch matches the attacker-supplied ref, it patches status.webhookCommit / status.lastWebhookTime and — when spec.pollingInterval is unset — patches the spec to pollingInterval: 1h, in namespaces where the caller holds no RBAC at all.

Exploit path

An attacker with network access to the webhook service — any in-cluster pod via the default ClusterIP Service, since no NetworkPolicy ships with the chart, or the internet once an Ingress is added for real git-host delivery — sends:

POST / HTTP/1.1
X-GitHub-Event: push

{"ref":"refs/heads/<victim-branch>","after":"<attacker SHA>",
 "repository":{"html_url":"<victim repo URL>"}}

with no X-Hub-Signature-256. The first (nil-secret) parse succeeds, a victim GitRepo in another namespace matches, getSecret returns nil, verification is skipped, and the controller patches the victim's status and polling interval.

Impact

Bounded to integrity and availability of Fleet's git-polling behaviour, and deliberately scoped as such in the report:

  • Integrity (Low): the attacker controls when a GitRepo reconciles. They do not control deployed content — the controller always re-resolves the real remote commit before deploying and abandons a webhookCommit that never appears there. No rollback, no content injection.
  • Availability (Low): the one-time pollingInterval → 1h flip suppresses polling and can delay pickup of legitimate changes by up to an hour; each request also drives git operations against the configured git host.
  • Authorization: a caller with no Kubernetes credentials mutates GitRepo spec and status cluster-wide, across namespaces they are not authorized for. No data is disclosed.

Adversarial re-read

  • The provider's crypto is sound. go-playground/webhooks uses hmac.Equal / subtle.ConstantTimeCompare and fails closed on a missing header; a control test with a secret present rejects the unsigned request. This finding is strictly about the no-secret default, not a signature bypass.
  • No content injection. webhookCommit is only a wake trigger, which caps integrity at Low and the finding at Moderate rather than High.
  • Not merely "don't expose it without a secret". It is reportable because it is the default (no secret templated, verification silently skipped, no warning), the ClusterIP Service is reachable by any in-cluster pod with no shipped NetworkPolicy, and it mutates a spec field cluster-wide across namespaces.
  • Distinct from GHSA-jmf4 (regex injection in the same function, already fixed): none of the prior Fleet advisories cover missing authentication or the optional-secret default.

Fix

Fleet now confines webhook-triggered mutation of GitRepo.spec to requests successfully verified against either GitRepo.spec.webhookSecret or the global gitjob-webhook Secret. Unverified requests can no longer set the default polling interval.

Workarounds for deployments that cannot upgrade: configure a webhook secret (per repository or globally), set spec.pollingInterval explicitly on each GitRepo, and restrict network access to the gitjob webhook port with a NetworkPolicy or authenticated ingress.

Write-up only. The httptest-based reproducer used during disclosure drove the real Webhook.ServeHTTP against a fake client in an unpatched checkout and is not republished here. Refer to the linked advisory for the vendor's own account and fixed releases.