TL;DR
Na 11 sad pravidel Firestore jsem poslal stejné požadavky jako host, anonymní uživatel a tři přihlášení uživatelé. request.auth != null nechalo cizího člověka číst, měnit i mazat cizí objednávky. Kontrola vlastníka jen přes resource nechala uživatele předat objednávku někomu jinému. Jeden široký match {document=**} otevřel kolekci, která měla správné pravidlo pro vlastníka. A role uložená v dokumentu, který uživatel smí zapisovat, z něj udělala admina jedním požadavkem. U každého případu je níže oprava a opravené pravidlo obstálo proti stejným požadavkům.
Mezi prohlížečem a databází aplikace na Firebase není žádný váš server. Konfigurace projektu je součástí stránky a pravidla zabezpečení jsou to jediné, co stojí mezi kýmkoli, kdo ji má, a vašimi daty. Chtěl jsem proto příklady pravidel s útokem hned vedle, ne seznam pravidel, která jen vypadají správně.
Jak jsem testoval. Firebase Local Emulator Suite z Firebase CLI 15.34.2, 3. listopadu 2026. Každou sadu pravidel jsem načetl, vynuloval data a volal REST API Firestore se skutečnými tokeny z emulátoru Auth: bez tokenu, anonymní uživatel, uživatelé A a B a cizí člověk, který se jen zaregistroval. Data byla vymyšlená: objednávky a1 a a2 patří A, b1 patří B.
Která pravidla Firebase propouštějí data?
| Pravidlo | Požadavek | Výsledek |
|---|---|---|
if true (testovací režim) |
Host vypíše objednávky | 200, všechny 3 |
if true (testovací režim) |
Host smaže objednávku B | 200 |
request.auth != null |
Anonymní uživatel vypíše objednávky | 200, všechny 3 |
request.auth != null |
Cizí uživatel smaže objednávku A | 200 |
Kontrola vlastníka jen přes resource |
A vytvoří objednávku patřící B | 200 |
Kontrola vlastníka jen přes resource |
A změní ownerId své objednávky |
200 |
Pravidlo vlastníka plus čtení {document=**} |
A přečte objednávku B | 200 |
Uživatel smí zapisovat svůj dokument users |
A nastaví role: admin, přečte objednávku B |
200 |
Co vlastně otevírá testovací režim?
match /{document=**} {
allow read, write: if true;
}
Všechno a všem. Host bez tokenu vypsal všechny 3 objednávky, jednu smazal a vytvořil novou. K ničemu z toho nepotřebuje účet ani uniklé tajemství, stačí id projektu.
Datovaná verze, kterou testovací režim často používá, selhává jinak. S if request.time < timestamp.date(2026, 1, 1) a datem v minulosti dostal host 403 a stejně tak uživatel A při čtení vlastní objednávky. Únik skončil, ale skončila i aplikace.
Stačí request.auth != null?
Ne. Říká jen, že za požadavkem stojí nějaký účet.
match /orders/{id} {
allow read, write: if request.auth != null;
}
Host dostal 403, proto toto pravidlo při rychlém testu vypadá bezpečně. Pak ale cizí člověk, který se jen zaregistroval, vypsal všechny 3 objednávky, změnil částku na objednávce B a smazal objednávku A, pokaždé s 200. Anonymní uživatel vypsal všechny 3 také. Pokud je registrace otevřená, znamená toto pravidlo „každý, kdo se obtěžuje zaregistrovat“. Dokumentace Firebase ho uvádí také jako častý nebezpečný vzor.
Proč kontrola vlastníka potřebuje resource i request.resource?
resource je dokument tak, jak je uložený. request.resource je dokument tak, jak by vypadal po zápisu. Pravidlo, které se dívá jen na první, kontroluje, komu řádek patří teď, a nic neříká o tom, na co ho uživatel přepíše.
match /orders/{id} {
allow read, update, delete: if resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null;
}
Čtení vypadalo správně: A dostal 200 na svou objednávku a 403 na objednávku B. Zápisy ne. A vytvořil novou objednávku s ownerId nastaveným na B a dostal 200. Pak A změnil ownerId na své objednávce na B, také 200, a B ji od té chvíle mohl číst. V aplikaci, kde objednávky znamenají faktury nebo zůstatky, to je uživatel, který zapisuje do cizího účtu.
Tato verze odmítla všechno:
match /orders/{id} {
allow read, delete: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null
&& request.resource.data.ownerId == request.auth.uid;
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.ownerId == resource.data.ownerId;
}
| Požadavek od A | Výsledek |
|---|---|
| Přečíst vlastní objednávku | 200 |
| Číst, měnit, mazat objednávku B | 403 |
| Vytvořit objednávku patřící B | 403 |
Změnit ownerId na B |
403 |
Stále ale dovolilo A změnit total na vlastní objednávce, s 200. Pokud klient to pole nastavovat nesmí, napište, která pole smí měnit:
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['note']);
Díky tomu změna total vrátila 403 a změna note vrátila 200.
Proč můj dotaz selhal, i když bylo pravidlo správné?
Protože pravidla nejsou filtry. Při výše uvedeném pravidle vlastníka dostal A na dotaz na celou kolekci 403, přestože vlastní 2 ze 3 objednávek. Firestore posuzuje dotaz podle toho, co by mohl vrátit, ne podle toho, co v databázi je.
| Požadavek od A | Výsledek |
|---|---|
| Vypsat všechny objednávky | 403 |
Dotaz ownerId == A |
200, 2 dok. |
Dotaz ownerId == B |
403 |
Když se tedy po zpřísnění pravidel rozbije výpis, opravte dotaz. Uvolňovat pravidlo, aby výpis fungoval, je přesně způsob, jak vznikají ta široká pravidla výše.
Může široký match zrušit správné pravidlo?
Ano, a na tohle bych se v projektu s dlouhým souborem pravidel díval jako na první. Firestore povolí požadavek, pokud ho povolí kterékoli odpovídající pravidlo.
match /orders/{id} {
allow read: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
}
match /{document=**} {
allow read: if request.auth != null;
}
První blok vypadá jako dobré pravidlo pro vlastníka. S přítomností druhého bloku A přečetl objednávku B s 200, cizí uživatel vypsal všechny 3 objednávky a stejný uživatel přečetl dokument A v users. První blok nijak nevaruje. Široký match smažte; pozdější pravidlo nemá jak přístup vzít zpět.
Jak si uživatel udělá admina?
Tento vzor se objevuje všude, kde jsou role v dokumentu:
function isAdmin() {
return get(/databases/$(database)/documents/users/$(request.auth.uid)).data.role == 'admin';
}
match /users/{uid} {
allow read, write: if request.auth.uid == uid;
}
match /orders/{id} {
allow read: if resource.data.ownerId == request.auth.uid || isAdmin();
}
„Uživatelé smějí upravovat svůj profil“ a „admini smějí číst všechny objednávky“ jsou každé samo o sobě rozumné. Dohromady je to zvýšení oprávnění.
Oprava je zabránit klientovi zapisovat to pole. Po přidání request.resource.data.role == resource.data.role do pravidla pro zápis vrátil stejný požadavek 403 a A zůstal mimo objednávku B. Druhou cestou jsou vlastní claims nastavené ze serveru, protože klient je změnit nemůže.
Dvě věci, které dopadly dobře
Pravidla na nadřazené cestě neprosakují do podkolekcí. S allow read na /orders/{id} pro libovolného přihlášeného uživatele A objednávku přečetl a /orders/b1/notes/n1 pod ní vrátila 403. To je zdokumentované chování: pravidla platí jen na odpovídající cestě.
Host neprojde přes porovnání s chybějícím tokenem. resource.data.ownerId == request.auth.uid vrátilo hostovi 403, i bez samostatného request.auth != null. Přesto tu kontrolu píšu, protože pak pravidlo říká, co znamená.
Jak zkontrolovat vlastní pravidla?
Spusťte své požadavky v emulátoru jako host, jako cizí uživatel a jako druhý uživatel. To odhalilo každý případ výše; čtení pravidel ne.
Pro první průchod čte bezplatná Kontrola pravidel Firebase vložená pravidla, aniž by se dotkla vašeho projektu. Na zde uvedených sadách označila testovací režim, přístup jen po přihlášení, široký match a roli, kterou si uživatel zapisuje sám. Čte jen text, takže nepozná, jak vypadají vaše data ani zda bylo pravidlo nasazeno.
Samotný apiKey ve stránce problém není. Webová konfigurace Firebase je veřejná záměrně a bezplatný Skener tajných klíčů ji tak označuje, protože data za ní chrání pravidla. Soubory se řídí zase jinými pravidly, s únikem, který pravidla zastavit neumějí, a ten jsem testoval v článku pravidla Firebase Storage.
Limity. Jen Firestore, v lokálním emulátoru, přes REST, na vymyšlených datech. Realtime Database používá jiný jazyk pravidel a netestoval jsem ji. App Check, který omezuje, které aplikace smějí volat váš backend, tu není; pravidla nenahrazuje. Produkce se od emulátoru může v detailech lišit, které jsem nekontroloval.