---
title: "Multi-Tenant Fail-Open Authorization Bug"
aliases: [fail-open-authz, multitenant-authz-bug, get-accessible-projects-none]
tags: [security, authorization, multitenant, fastapi, python, bug]
sources:
  - "daily/2026-04-29.md"
created: 2026-04-29
updated: 2026-04-29
---

# Multi-Tenant Fail-Open Authorization Bug

In multi-tenant systems, a function that retrieves accessible resource IDs for a user must return an empty list `[]` (deny-all) when the user has no org assignments — never `None`. Returning `None` is typically interpreted downstream as "no filter applied" (fail-open), giving the user unrestricted access to all tenants' data. This is a critical security vulnerability pattern that is easy to introduce and hard to detect through normal testing.

## Key Points

- `get_accessible_project_ids()` returning `None` means "no restriction" → fail-open (every project visible)
- `get_accessible_project_ids()` returning `[]` means "deny all" → fail-closed (correct behavior)
- Always use `[]` as the default return when a user has no assignments, never `None` or omitting a return statement
- Use **404 Not Found** (not 403 Forbidden) for cross-tenant resource access — returning 403 confirms the resource exists, which is an information disclosure vulnerability
- Add a mandatory guard at the access-check boundary: `if project_ids is None: raise ValueError("BUG: authz returned None")`

## Details

### The Vulnerable Pattern

```python
# ❌ DANGEROUS — returns None when no assignments exist
async def get_accessible_project_ids(user_id: str, db) -> list[str] | None:
    assignments = await db.org_assignments.find({"user_id": user_id}).to_list(None)
    if not assignments:
        return None  # ← Bug: caller treats None as "no filter"
    return [a["project_id"] for a in assignments]

# ❌ Caller fails open
async def list_projects(user_id: str, db):
    project_ids = await get_accessible_project_ids(user_id, db)
    query = {}
    if project_ids is not None:   # ← None passes this check!
        query["_id"] = {"$in": project_ids}
    return await db.projects.find(query).to_list(None)  # Returns ALL projects
```

### The Correct Pattern

```python
# ✅ SAFE — always returns a list, empty list = deny all
async def get_accessible_project_ids(user_id: str, db) -> list[str]:
    assignments = await db.org_assignments.find({"user_id": user_id}).to_list(None)
    if not assignments:
        return []  # ← Explicit deny-all
    return [a["project_id"] for a in assignments]

# ✅ Caller fails closed
async def list_projects(user_id: str, db):
    project_ids = await get_accessible_project_ids(user_id, db)
    # Empty list → $in: [] → MongoDB returns zero documents ✓
    return await db.projects.find({"_id": {"$in": project_ids}}).to_list(None)
```

### Cross-Tenant Access: 404 Not 403

```python
# ❌ Leaks existence of the resource
async def get_project(project_id: str, user_id: str, db):
    project = await db.projects.find_one({"_id": project_id})
    if not project:
        raise HTTPException(404)
    if project["tenant_id"] != user_tenant(user_id):
        raise HTTPException(403, "Forbidden")  # ← Tells attacker the project exists!

# ✅ 404 for all cross-tenant access — no existence disclosure
async def get_project(project_id: str, user_id: str, db):
    project = await db.projects.find_one({
        "_id": project_id,
        "tenant_id": user_tenant(user_id)   # ← Filter by tenant in the query itself
    })
    if not project:
        raise HTTPException(404)  # Same response whether not found OR wrong tenant
```

### Defensive Guard at Boundary

```python
# Add a guard wherever authz results are consumed
project_ids = await get_accessible_project_ids(user_id, db)
assert project_ids is not None, "BUG: authz function returned None — fail-open risk"
```

## Related Concepts

- [[wiki/concepts/azure-ad-yaml-allowlist-pattern]] — related pattern for YAML-based authorization
- [[wiki/concepts/security-middleware-json-key-regex]] — companion security article from the same session

## Sources

- [[daily/2026-04-29.md]] — Discovered during multi-tenant authz review of a FastAPI project
