How to Detect Cross-Tenant Data Leakage in MCP Servers and Multi-Tenant SaaS
The Hidden Security Gap in Multi-Tenant MCP Servers When you build a multi-tenant SaaS application or an MCP (Model Context Protocol) server that serves multiple organizations, cross-tenant data leakage is one of the most dangerous vulnerabilities you can ship. A single missing organizationId filter in a database query can expose one tenant's data to another — and traditional security scanners like Snyk, Semgrep, and CodeQL don't catch these patterns. That's why I built mcp-tenant-isolation — a static analysis scanner with 57 deterministic rules specifically designed to catch tenant isolation failures in multi-tenant codebases. What Is Tenant Isolation? Tenant isolation ensures that data belonging to one organization (tenant) is never accessible to another. In a multi-tenant SaaS app, every database query, cache read, and file access must be scoped to the current tenant's organizationId. The most common failure looks like this: // VULNERABLE: No organizationId filter const users = await prisma.user.findMany({ where: { role: 'admin' } }); // SECURE: Tenant-scoped query const users = await prisma.user.findMany({ where: { role: 'admin', organizationId: ctx.orgId } }); It looks obvious in isolation. But in a codebase with 100+ API routes, dozens of lib functions, and complex middleware chains, missing tenant filters are easy to miss in code review and impossible for traditional SAST tools to detect. Why Traditional Scanners Miss This Tools like Snyk and Semgrep are excellent at detecting: SQL injection XSS Dependency vulnerabilities Secret leakage But they don't understand tenant context. They don't know that organizationId is the tenant boundary. They don't track which functions require tenant guards. They can't tell you that prisma.user.findMany({ where: { role: 'admin' } }) is missing a critical tenant filter. mcp-tenant-isolation fills this gap with 57 rules across 7 categories: Rule Categories Category Rules What It Detects Database Queries (DBQ) 12 Missing organizationId in findMany, findUnique, updateMany, deleteMany, groupBy, aggregate Schema (SCH) 6 Missing tenant columns, missing composite indexes, missing RLS policies File Storage Isolation (FSI) 4 Unscoped S3/Blob keys, missing tenant prefix in file paths Cache Key Scoping (CKS) 5 Redis/memoization keys without tenant prefix IDOR 8 Insecure direct object references — accessing resources by ID without ownership check Logging (LOG) 6 Missing tenant context in structured logs MCP-Specific (MCP) 15 Tool visibility, cache prefix, session binding, credential vault isolation, prompt injection surface Quick Start Install npm install -g mcp-tenant-isolation Scan your codebase mti scan ./src Output The scanner produces a clear pass/fail verdict with detailed findings: ╔══════════════════════════════════════════╗ ║ MCP Tenant Isolation Scanner v1.6.2 ║ ║ Verdict: FAIL ║ ╚══════════════════════════════════════════╝ Findings: 47 active, 12 suppressed, 3 baseline DBQ-001 HIGH app/api/users/route.ts:23 Missing organizationId in prisma.user.findMany() Remediation: Add organizationId to the where clause: where: { ..., organizationId: ctx.orgId } IDOR-003 HIGH app/api/documents/[id]/route.ts:45 No ownership check after findUnique by ID Remediation: Verify result.organizationId === ctx.orgId CI/CD Integration GitHub Actions (Pre-built Action) - uses: subodhkc/mcp-tenant-isolation@v1 with: path: ./src format: sarif Manual npx - name: Run tenant isolation scan run: npx mcp-tenant-isolation scan ./src --format sarif --output scan.sarif - name: Upload to GitHub Code Scanning uses: github/codeql-action/upload-sarif@v3 with: sarif_file: scan.sarif SARIF output integrates directly with GitHub Advanced Security — findings appear in your repo's Security tab alongside CodeQL results. MCP Server Integration The package includes an MCP server with 4 tools that AI agents (Claude Desktop, Cursor, Cline) can use: { "mcpServers": { "mcp-tenant-isolation": { "command": "npx", "args": ["-y", "mcp-tenant-isolation", "mcp"] } } } Tools available: scan — Run tenant isolation scan on a codebase rules — List all 57 rules with descriptions and remediation hints config — Get/set scanner configuration version — Get scanner version info This means you can ask Claude: "Scan my src directory for tenant isolation issues" and get structured findings with remediation hints — directly in your chat. Supported Frameworks ORMs: Prisma, Drizzle, raw SQL (Knex, pg, mysql2) Web frameworks: Next.js (App Router & Pages), Express, Fastify Cache: Redis (ioredis, redis), in-memory memoization Storage: S3, Vercel Blob, local filesystem Auth: NextAuth, custom JWT, session-based Output Formats Format Use Case Terminal Local development with pass/fail verdict JSON Programmatic consumption, custom dashboards SARIF 2.1.0 GitHub Code Scanning, Azure DevOps AI JSON AI agent consumption with remediation hints Markdown Shareable reports for PRs and team review Suppression and Baselines Not every finding is actionable immediately. The scanner supports: Suppressions — .mtirc.json config to suppress specific findings with reasons Baselines — Mark existing findings as known debt, only fail CI on new issues Rule packs — Enable/disable rule categories per project { "suppress": [ { "rule": "DBQ-001", "file": "app/api/public/badges/route.ts", "reason": "Public route, no tenant context needed" } ] } Real-World Results I ran the scanner against a production Next.js SaaS codebase with 200+ API routes: 47 active findings — including 23 missing tenant filters, 8 IDOR risks, 5 unscoped cache keys 12 suppressed — public routes with legitimate no-tenant-context access 3 baselined — known debt scheduled for next sprint Scan time: 4.2 seconds for 50,000+ lines of TypeScript Why This Matters for MCP Server Developers If you're building MCP servers that serve multiple tenants (organizations, teams, users), tenant isolation is the security boundary. The MCP protocol doesn't enforce tenant isolation — it's up to your implementation. The 15 MCP-specific rules check for: Tool visibility — Are all tools visible to all tenants? Should some be org-scoped? Cache prefix — Is the MCP response cache keyed by tenant? Session binding — Is the MCP session bound to a specific tenant? Credential vault — Are per-tenant credentials isolated in the vault? Prompt injection surface — Does the server expose tenant context to prompt injection? Links GitHub: subodhkc/mcp-tenant-isolation npm: mcp-tenant-isolation Landing page: haiec.com/mcp-tenant-isolation MCP Registry: io.github.subodhkc/mcp-tenant-isolation License: MIT If you found this useful, star the repo on GitHub and share with your team. Every missing tenant filter is a potential data breach waiting to happen.
This is a summary aggregated from Dev.to. Read the complete article on the original site:
Read full article at Dev.to