DEV Community

Subodh Kc
Subodh Kc

Posted on • Originally published at haiec.com

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 }
});
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Scan your codebase

mti scan ./src
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

CI/CD Integration

GitHub Actions (Pre-built Action)

- uses: subodhkc/mcp-tenant-isolation@v1
  with:
    path: ./src
    format: sarif
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"]
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

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


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.

Top comments (0)