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…

Detection rules are retrieved from Rulezet.

Loading…

Loading…

Loading…