T09 · Insecure Skill Coding Practices
Error
- Location
- SKILL.md:1078
- Finding
- Broken Tenant Isolation in Recommended Multi-Tenant Pattern<![CDATA[ ## Vulnerability Details **File Location**: `SKILL.md:1078-1099` **Vulnerability Type**: Tenant authorization bypass and ineffective queryset isolation **Risk Level**: High ### Vulnerable Code ```python # Middleware: set tenant from request class TenantMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): tenant_id = request.headers.get("X-Tenant-ID") if tenant_id: request.tenant = Tenant.objects.get(id=tenant_id) return self.get_response(request) # Auto-filter all queries by tenant class TenantManager(models.Manager): def get_queryset(self): from threading import local _thread_local = local() qs = super().get_queryset() tenant = getattr(_thread_local, "tenant", None) if tenant: qs = qs.filter(tenant=tenant) return qs ``` ### Technical Analysis The middleware trusts the client-controlled `X-Tenant-ID` header and retrieves the corresponding tenant without verifying that the authenticated user is authorized to access it. This allows a caller to attempt tenant selection using an arbitrary tenant identifier. The advertised automatic filtering mechanism is also ineffective. `TenantManager.get_queryset()` creates a new `threading.local()` object every time it is called. No tenant value is written to this new object, and it has no connection to `request.tenant`. Consequently, `tenant` is normally `None`, the conditional filter is skipped, and the unscoped base queryset is returned. This violates the central security requirement of a multi-tenant application: tenant boundaries must be enforced through authenticated authorization and fail-closed data scoping. Although this is documentation rather than directly executable project code, the Skill presents the pattern as reusable production guidance. Applications generated from or modeled on it could therefore inherit the vulnerabilit ...[truncated 1397 chars]
- Remediation
- <![CDATA[ ## Remediation Suggestions 1. Never authorize tenant access solely from a client-provided identifier. 2. Resolve the requested tenant through an authenticated membership relationship, for example: ```python tenant = request.user.tenants.filter(id=tenant_id).first() if tenant is None: raise PermissionDenied("User is not authorized for this tenant.") request.tenant = tenant ``` 3. Fail closed when the tenant header or authenticated tenant context is missing. Do not return an unfiltered queryset. 4. Avoid creating a new `threading.local()` instance inside `get_queryset()`. If implicit request context is unavoidable, use a correctly managed `contextvars.ContextVar` that is set and reset by middleware. 5. Prefer explicit tenant-scoped service and queryset APIs, such as `Model.objects.for_tenant(request.tenant)`, so the security boundary is visible and testable. 6. Apply object-level authorization in addition to queryset filtering. 7. Validate that related-object lookups, bulk operations, background tasks, and administrative interfaces also enforce the same tenant boundary. 8. Add negative tests proving that a user from tenant A cannot read, update, or delete tenant B's records, including when tenant headers are omitted, altered, or malformed. ]]>
