CVE case study

CVE-2026-59979: Authentication Is Not Tenant Authorization

How authentication-only routes and globally keyed service lookups enabled cross-tenant reads and state changes in Yosemite-Crew.

This is a defensive case study of CVE-2026-59979, based on the coordinated public advisory. It explains the trust boundary, the failure mode, and the engineering fix without reproducing a weaponized exploit.

Severity
High · 8.3
Scoring
CVSS 3.1
Weakness
CWE-639 · CWE-862
Affected
Main branch through bc1a1824b1a7eba2bc61176cdb61d9a2a11bce57
Remediation state
No complete fixed release is listed by the advisory
Advisory published
21 Jun 2026

Official vectorCVSS:3.1/AV:N/AC:L/PR:L/UI:N/S:U/C:L/I:H/A:H

Why it matters

Several routes established who the caller was but did not establish whether that caller belonged to the organization owning the requested object or held the permission required for the operation. One disclosed read path lacked authentication entirely.

The service layer then looked up records by global identifiers without an organization constraint. In a multi-tenant application, that combination turns predictable or discovered object references into cross-tenant capabilities.

How I discovered it

I used a route-middleware census rather than testing endpoints at random. Yosemite-Crew had recently added organization and permission middleware to new service routes, so I compared create, read, update, and delete siblings in the same router.

The create routes carried authentication, withOrgPermissions(), and requirePermission(). The update and delete routes carried only authorizeCognito, while the single-service read route had no authentication middleware. I opened authorizeCognito to avoid assuming its name implied authorization; it verified the token and set userId, but performed no organization or role lookup.

I then followed :id through the controller. No tenant identity was passed to the service, and the service used a global findById(). Repeating the same census in the PMS task library and template routes revealed the same authentication-only pattern. The finding was therefore not one forgotten endpoint but an incomplete RBAC retrofit across a route family.

Trust boundary and root cause

Yosemite-Crew had separate middleware for Cognito authentication, organization membership, and role permission. Some create routes used the full stack, while related read, update, delete, library, and template routes used authentication only or no guard.

Authorization was therefore inconsistent at the route layer and absent as defense in depth at the data-access layer. The backend accepted a globally supplied record ID as enough context to read or mutate the object.

Source-to-sink trace

  1. 01
    Intent controlservice.router.ts · create routes

    Sibling operations use authentication, organization membership, and an explicit permission requirement.

  2. 02
    Missing route guardservice.router.ts · GET/PATCH/DELETE /:id

    Read has no guard; update and delete use authentication only.

  3. 03
    Identity-only middlewaremiddlewares/auth.ts · authorizeCognito

    The middleware establishes user identity but not tenant membership or function permission.

  4. 04
    Unscoped data sinkservice.service.ts · findById()

    The controller supplies only a global object ID, and the service lookup does not filter by organisationId.

Cross-tenant object accessIdentity is established, but the requested object is never bound to the caller’s organization and permission set.
  1. 01
    Low-privilege identity

    A registered caller reaches an affected service, task-library, or template route.

  2. 02
    Foreign object key

    The request names a record belonging to another organization.

  3. 03
    Incomplete middleware

    The route authenticates the caller but omits organization and function-level authorization.

  4. 04
    Global lookup

    The service reads, changes, or deletes the record by ID without tenant scoping.

Safe proof of concept

Prerequisites

  • Git and rg installed locally.
  • A clone of the public Yosemite-Crew repository; no account, token, database, or hosted target is needed.

Step-by-step reproduction

  1. Clone the public repository and check out bc1a1824b1a7eba2bc61176cdb61d9a2a11bce57, the revision named by the advisory.
  2. Run the assertions below. They verify the full RBAC stack on sibling create routes, authentication-only update/delete routes, and the unguarded read route.
  3. Open authorizeCognito and confirm that it establishes userId but performs no organization or permission lookup.
  4. Trace updateService from router to controller to service. Confirm that only :id and the DTO reach a global findById() query.
  5. Repeat the middleware census for task-library and template routes; this establishes the affected family rather than stopping at one service endpoint.
  6. If you build an optional runtime lab, seed two synthetic organizations through the project’s supported fixtures, obtain a valid local token, fetch a complete valid ServiceRequestDTO, change one benign field, and PATCH that full DTO. Do not use a one-field body or a production record.

Clone the audited revision

git clone https://github.com/YosemiteCrew/Yosemite-Crew.git yosemite-cve-lab
cd yosemite-cve-lab
git checkout bc1a1824b1a7eba2bc61176cdb61d9a2a11bce57

Offline authorization assertions

SERVICE_ROUTER=apps/backend/src/routers/service.router.ts
AUTH=apps/backend/src/middlewares/auth.ts
SERVICE=apps/backend/src/services/service.service.ts
TASK_ROUTER=apps/backend/src/routers/task.router.ts

# Intent control: sibling create routes have organization + permission guards.
rg -n 'withOrgPermissions|requirePermission\("specialities:edit:any"\)' "$SERVICE_ROUTER"

# Gap: read is unguarded; update/delete are authentication-only.
rg -n 'router\.(get|patch|delete)\("/:id"' "$SERVICE_ROUTER"

# Authentication establishes identity, not tenant or role authorization.
rg -n 'authorizeCognito|authReq.userId = payload.sub' "$AUTH"

# Controller/service path reaches a global lookup with no tenant key.
rg -n 'updateService|findById\(oid\)|organisationId' "$SERVICE"

# Same census for the task library/template family.
rg -n 'pms/library|pms/templates|withTaskOrgPermissions|requirePermission' "$TASK_ROUTER"

Expected evidence

  • The output shows the complete authorization stack on sibling create/task operations and its absence from the disclosed read/update/delete family.
  • The authentication middleware sets caller identity without loading organization membership or permissions.
  • The update path reaches findById() without an organisationId predicate; the proof contacts no application endpoint and changes no data.
Negative control

The sibling create and canonical PMS task routes in the same files carry organization and explicit permission middleware. That in-source control proves the application has an RBAC design and isolates the missing guards as an inconsistent route-family retrofit.

Fixed-version re-test

The vendor advisory still lists no complete fixed release. A candidate fix must add authentication, organization membership, permission enforcement, and organisationId-scoped queries across every named route family. Only then should an owned two-tenant integration test assert 401, 403, or a non-enumerating 404 and an unchanged synthetic record.

Impact in context

The advisory documents cross-tenant disclosure, modification, and deletion of service-catalogue and PMS task-definition data. Integrity and availability impact are high because state-changing operations can affect downstream workflows such as bookings and billing.

Most affected write paths require only a low-privilege account and a target identifier. The disclosed service-by-ID read path was reachable without authentication, further demonstrating why every object operation needs its own policy check.

Remediation and verification

Apply authentication, organization membership, and explicit permission middleware to every affected route. Backend methods should also accept the tenant identity and query by both object ID and organisationId, so a missed route guard fails closed.

As of this review, the vendor advisory lists no fixed release. Public source shows partial hardening of service update and delete paths, but it does not justify claiming the entire disclosed route family is fixed. Deployment owners should compare their version with the vendor advisory and verify every named operation.

Engineering lesson

Authorization matrices should be tested as a complete family: create, list, read, update, delete, archive, and template/library variants. A correct guard on one method says nothing about its siblings, and authentication middleware is never a substitute for tenant and function-level authorization.

Primary public references

Disclosure boundary: only already-public technical detail is included here. Unpublished validation material, private communications, secrets, and weaponized payloads remain excluded.