- 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 thisspecmutation 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.
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.
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.
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
webhookCommitthat never appears there. No rollback, no content injection. - Availability (Low): the one-time
pollingInterval → 1hflip 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.
- The provider's crypto is sound.
go-playground/webhooksuseshmac.Equal/subtle.ConstantTimeCompareand 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.
webhookCommitis 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.
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.ServeHTTPagainst 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.