Přeskočit na obsah

Multi-tenant — module-level in-memory cache leakuje data mezi tenanty

import { Aside } from ‘@astrojs/starlight/components’;

Po zavedení multi-tenant architektury (databáze per tenant, resolvace přes subdoménu) vracel endpoint pro tenant B data tenanta A — v našem případě branding aplikace, ale stejný vzor postihoval i fakturační údaje firmy, RBAC práva rolí a feature flagy. Projevilo se až E2E smoke testem se dvěma tenanty; unit testy i build byly zelené.

Single-tenant aplikace léta bezpečně používala module-level in-memory cache s TTL — modul žije jednou v celém Node procesu, takže cache byla implicitně „per aplikace = per zákazník”:

// PŘED — bezpečné v single-tenant, kritický leak v multi-tenant
let cachedConfig: Config | null = null;
let cacheExpiry = 0;
export async function getConfig(): Promise<Config> {
if (cachedConfig && Date.now() < cacheExpiry) return cachedConfig;
cachedConfig = await db.setting.findUnique(...); // dotaz do TENANT DB
cacheExpiry = Date.now() + 5 * 60 * 1000;
return cachedConfig;
}

V multi-tenant režimu obsluhuje tentýž proces požadavky všech tenantů. První tenant cache naplní daty ze SVÉ databáze a dalších 5 minut je dostávají všichni ostatní. Zvlášť zákeřné u RBAC: cache práv klíčovaná jen kódem role ("ADMIN") aplikovala oprávnění tenanta A u tenanta B, protože kódy rolí napříč tenant databázemi kolidují.

Centrální helper vracející klíč aktuálního tenanta (z request kontextu / AsyncLocalStorage; v single-tenant režimu konstantu), a všechny cache klíčovat přes něj:

// PO — cache klíčovaná tenantem
const configCache = new Map<string, { value: Config; expiry: number }>();
export async function getConfig(): Promise<Config> {
const tk = tenantCacheKey(); // dbName tenanta | "default" v single-tenant
const cached = configCache.get(tk);
if (cached && Date.now() < cached.expiry) return cached.value;
const value = await db.setting.findUnique(...);
configCache.set(tk, { value, expiry: Date.now() + 5 * 60 * 1000 });
return value;
}

U kompozitních klíčů prefixovat: `${tenantCacheKey()}:${role}`. Invalidační funkce nejjednodušeji cache.clear() (mění se zřídka).

Terminál
grep -rn 'let .*[Cc]ache\|const .*[Cc]ache.*=.*Map\|cacheExpiry\|CACHE_TTL' src --include='*.ts'

Každý nález, který čte z tenant DB, je kandidát. Pozor i na cache třetích stran (session, tokeny externích API) — tam rozhoduje, zda jsou credentials per tenant, nebo per proces.

  • Fyzická izolace DB per tenant tuto třídu chyb NEřeší — leak vzniká v aplikační vrstvě nad ní.
  • Unit testy to nechytí (běží single-tenant); nutný E2E test se dvěma tenanty a ROZDÍLNÝMI daty + assert, že tenant B nikdy nevidí hodnoty tenanta A.
  • Helper držet v modulu, který testy nemockují (u nás oddělený od Prisma klienta, protože jest.mock celého DB modulu by helper zabil).
Přidal aiarchitekt.cz · 14. 7. 2026 2:00
Provozuje aiarchitekt.cz