Audits EF Core tenant isolation at the data layer: verifies HasQueryFilter is applied to every entity with TenantId or OrgId, flags IgnoreQueryFilters calls without a justification comment, generates a cross-tenant test scaffold asserting Tenant A cannot read Tenant B's rows, and documents SQL Server row-level security as a defence-in-depth option. Outputs findings with pilot-sql multitenancy standard IDs.
Scanned 9/6/2026
Install to Claude Code
npx -y skills add AgenticPawan/FullStack-Pilot --skill sql-multitenancy --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Sql Multitenancy?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/agenticpawan-sql-multitenancy)More formats (shields.io, HTML) on the badges page.
---
name: sql-multitenancy
description: "Audits EF Core tenant isolation at the data layer: verifies HasQueryFilter is applied to every entity with TenantId or OrgId, flags IgnoreQueryFilters calls without a justification comment, generates a cross-tenant test scaffold asserting Tenant A cannot read Tenant B's rows, and documents SQL Server row-level security as a defence-in-depth option. Outputs findings with pilot-sql multitenancy standard IDs."
when_to_use: tenant isolation, multitenancy, global query filter, HasQueryFilter, IgnoreQueryFilters, TenantId, cross-tenant, row-level security, RLS, data isolation, tenant filter, tenant context
---
## Standard IDs
| ID | Severity | What it checks |
|----|----------|----------------|
| MT-001 | P0 | Entity with TenantId/OrgId has no HasQueryFilter |
| MT-002 | P0 | IgnoreQueryFilters() called without a justification comment |
| MT-003 | P2 | No cross-tenant isolation test exists for a tenant-filtered entity |
| MT-004 | P3 | Tenant filter uses a hard-coded value instead of a scoped service |
---
## Check A — HasQueryFilter coverage
### Detection
1. Glob `**/*.cs` for entity classes with a `TenantId` or `OrgId` property.
2. In `OnModelCreating`, check for `modelBuilder.Entity<EntityType>().HasQueryFilter(...)` for each such entity.
3. If any entity is missing its filter → MT-001.
### BAD — entity with TenantId, no filter
```csharp
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>().ToTable("Orders");
// Missing: .HasQueryFilter(o => o.TenantId == _tenant.CurrentTenantId)
}
```
### GOOD — filter applied via injected tenant context
```csharp
public class AppDbContext : DbContext
{
private readonly ITenantContext _tenant;
public AppDbContext(DbContextOptions<AppDbContext> options, ITenantContext tenant)
: base(options)
{
_tenant = tenant;
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<Order>()
.HasQueryFilter(o => o.TenantId == _tenant.CurrentTenantId);
modelBuilder.Entity<User>()
.HasQueryFilter(u => u.TenantId == _tenant.CurrentTenantId);
}
}
```
---
## Check B — IgnoreQueryFilters policy
Any call to `IgnoreQueryFilters()` must have a justification comment on the preceding line
explaining why cross-tenant access is intentional (e.g., admin report, system background job).
```csharp
// BAD: no justification — any developer can bypass tenant isolation
var allOrders = await _db.Orders.IgnoreQueryFilters().ToListAsync();
// GOOD: justification present
// Admin aggregate report — intentional cross-tenant read, caller must have Admin role
var allOrders = await _db.Orders.IgnoreQueryFilters().ToListAsync();
```
**Detection rule:** scan for `.IgnoreQueryFilters()` where the preceding code line does not
contain a `//` comment or `[Authorize(Roles = "Admin")]` attribute within 3 lines above.
---
## Check C — Cross-tenant test scaffold
When MT-001 is fixed, generate a test scaffold. If the fix is already in place, check whether
a corresponding cross-tenant test exists.
### Detection
Look for test files (in `**/*.Tests/**/*.cs` or `**/*Tests.cs`) that test the entities
identified in Check A. If no test asserts that Tenant B's data is inaccessible to Tenant A,
emit MT-003.
### Generated test scaffold
```csharp
public class OrderTenantIsolationTests : IClassFixture<AppDbContextFixture>
{
private readonly AppDbContextFixture _fixture;
public OrderTenantIsolationTests(AppDbContextFixture fixture)
=> _fixture = fixture;
[Fact]
public async Task Orders_AreIsolatedByTenant()
{
// Arrange — seed orders for two tenants
var tenantA = 1;
var tenantB = 2;
await _fixture.SeedOrderAsync(tenantId: tenantA, orderId: 100);
await _fixture.SeedOrderAsync(tenantId: tenantB, orderId: 200);
// Act — query as Tenant A
using var ctx = _fixture.CreateContext(tenantId: tenantA);
var orders = await ctx.Orders.ToListAsync();
// Assert — Tenant A sees only its own orders
Assert.All(orders, o => Assert.Equal(tenantA, o.TenantId));
Assert.DoesNotContain(orders, o => o.Id == 200);
}
[Fact]
public async Task Orders_IgnoreQueryFilters_RequiresExplicitCallsite()
{
// Verify the filter is active by default (regression guard)
using var ctx = _fixture.CreateContext(tenantId: 1);
var count = await ctx.Orders.CountAsync();
var countUnfiltered = await ctx.Orders.IgnoreQueryFilters().CountAsync();
Assert.True(countUnfiltered >= count, "IgnoreQueryFilters should surface more rows");
}
}
```
---
## Check D — SQL Server Row-Level Security (defence in depth)
Row-Level Security (RLS) enforces tenant isolation at the database engine level,
independent of application code. Recommend it when:
- The project is multi-tenant with strict data separation requirements
- The DbContext is accessed from multiple application tiers or tools (e.g., SSRS, ETL)
RLS adds a security predicate function and binds it to the table:
```sql
CREATE FUNCTION dbo.fn_tenantPredicate(@TenantId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS RETURN
SELECT 1 AS result
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TenantId') AS INT);
CREATE SECURITY POLICY TenantFilter
ADD FILTER PREDICATE dbo.fn_tenantPredicate(TenantId) ON dbo.Orders,
ADD BLOCK PREDICATE dbo.fn_tenantPredicate(TenantId) ON dbo.Orders;
```
Emit this as a P3 advisory finding — not a blocker, but a recommended hardening step.
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!