TL;DR
Lähetin samat pyynnöt 11 Firestore-sääntöjoukkoon vieraana, anonyymina käyttäjänä ja kolmena kirjautuneena käyttäjänä. request.auth != null salli ulkopuolisen lukea, muuttaa ja poistaa kaikkien tilaukset. Omistajatarkistus pelkän resource-kentän varassa antoi käyttäjän luovuttaa tilauksen jollekulle muulle. Yksi laaja {document=**} avasi kokoelman, jolla oli oikea omistajasääntö. Ja dokumenttiin tallennettu rooli, jota käyttäjä saa kirjoittaa, teki hänestä adminin yhdellä pyynnöllä. Jokaiseen on alla korjaus, ja korjattu sääntö kesti samat pyynnöt.
Firebase-sovelluksen selaimen ja tietokannan välissä ei ole sinun palvelintasi. Projektin konfiguraatio kulkee sivun mukana, ja tietoturvasäännöt ovat se, mikä seisoo datasi ja jokaisen sen konfiguraation haltijan välissä. Siksi halusin esimerkkejä säännöistä hyökkäys vierellään, en listaa sääntöjä, jotka vain näyttävät oikeilta.
Näin testasin. Firebase Local Emulator Suite Firebase CLI 15.34.2:sta, 3. marraskuuta 2026. Latasin jokaisen sääntöjoukon, nollasin datan ja kutsuin Firestoren REST API:a oikeilla tokeneilla Auth-emulaattorista: ei tokenia, anonyymi käyttäjä, käyttäjät A ja B sekä ulkopuolinen, joka oli vain rekisteröitynyt. Data oli keksittyä: A:n tilaukset a1 ja a2, B:n tilaus b1.
Mitkä Firebase-säännöt vuotavat?
| Sääntö | Pyyntö | Tulos |
|---|---|---|
if true (testitila) |
Vieras listaa tilaukset | 200, kaikki 3 |
if true (testitila) |
Vieras poistaa B:n tilauksen | 200 |
request.auth != null |
Anonyymi käyttäjä listaa tilaukset | 200, kaikki 3 |
request.auth != null |
Ulkopuolinen poistaa A:n tilauksen | 200 |
Omistajatarkistus vain resource |
A luo tilauksen, jonka omistaja on B | 200 |
Omistajatarkistus vain resource |
A muuttaa oman tilauksensa ownerId:n |
200 |
Omistajasääntö ja {document=**}-luku |
A lukee B:n tilauksen | 200 |
Käyttäjä saa kirjoittaa oman users-dokumenttinsa |
A asettaa role: admin ja lukee B:n tilauksen |
200 |
Mitä testitila oikeasti avaa?
match /{document=**} {
allow read, write: if true;
}
Kaiken, kaikille. Vieras ilman tokenia listasi kaikki 3 tilausta, poisti yhden ja loi uuden. Mikään tästä ei vaadi tiliä tai vuotanutta salaisuutta, vain projektin id:n.
Päivämäärällinen versio, jota testitila usein käyttää, epäonnistuu eri tavalla. Kun säännössä on if request.time < timestamp.date(2026, 1, 1) ja päivämäärä on mennyt, vieras sai 403 ja niin sai käyttäjä A lukiessaan omaa tilaustaan. Vuoto on ohi, ja sovellus myös.
Riittääkö request.auth != null?
Ei. Se kertoo vain, että pyynnön takana on jokin tili.
match /orders/{id} {
allow read, write: if request.auth != null;
}
Vieras sai 403, minkä vuoksi tämä sääntö tuntuu turvalliselta nopeassa testissä. Sitten ulkopuolinen, joka oli vain rekisteröitynyt, listasi kaikki 3 tilausta, muutti B:n tilauksen summaa ja poisti A:n tilauksen, kaikki 200:lla. Myös anonyymi käyttäjä listasi kaikki 3. Jos rekisteröityminen on auki, tämä sääntö tarkoittaa “kaikki, jotka viitsivät rekisteröityä”. Firebasen dokumentaatio listaa sen myös yleisten turvattomien mallien joukossa.
Miksi omistajatarkistus tarvitsee sekä resource- että request.resource-kentän?
resource on dokumentti sellaisena kuin se on tallennettu. request.resource on dokumentti sellaisena kuin se olisi kirjoituksen jälkeen. Sääntö, joka katsoo vain ensimmäistä, tarkistaa kenen rivi on nyt, eikä sano mitään siitä, miksi käyttäjä sen muuttaa.
match /orders/{id} {
allow read, update, delete: if resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null;
}
Lukemiset näyttivät oikeilta: A sai 200 omasta tilauksestaan ja 403 B:n tilauksesta. Kirjoitukset eivät. A loi uuden tilauksen, jonka ownerId oli B, ja sai 200. Sitten A muutti oman tilauksensa ownerId:n B:ksi, myös 200, ja B saattoi lukea sen siitä lähtien. Sovelluksessa, jossa tilaukset ovat laskuja tai saldoja, tämä on käyttäjä, joka kirjoittaa jonkun toisen tilille.
Tämä versio kieltäytyi kaikesta:
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;
}
| A:n pyyntö | Tulos |
|---|---|
| Lue oma tilaus | 200 |
| Lue, päivitä, poista B:n tilaus | 403 |
| Luo tilaus, jonka omistaja on B | 403 |
Muuta ownerId B:ksi |
403 |
Se salli silti A:n muuttaa oman tilauksensa total-kentän, 200:lla. Jos asiakas ei saa asettaa sitä kenttää, sano, mitä kenttiä hän saa muuttaa:
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['note']);
Tällä total:n muuttaminen palautti 403 ja note:n muuttaminen 200.
Miksi kyselyni epäonnistui oikealla säännöllä?
Koska säännöt eivät ole suodattimia. Yllä olevalla omistajasäännöllä A sai 403, kun pyysi koko kokoelmaa, vaikka A omistaa 2 tilausta kolmesta. Firestore arvioi kyselyn sen mukaan, mitä se voisi palauttaa, ei sen mukaan, mitä siellä on.
| A:n pyyntö | Tulos |
|---|---|
| Listaa kaikki tilaukset | 403 |
Kysely ownerId == A |
200, 2 dok. |
Kysely ownerId == B |
403 |
Kun lista hajoaa sääntöjen tiukentamisen jälkeen, korjaa kysely. Säännön löysääminen, jotta lista toimisi, on tapa, jolla yllä olevat laajat säännöt syntyvät.
Voiko laaja match kumota oikean säännön?
Voi, ja tätä etsisin ensimmäisenä projektista, jossa on pitkä sääntötiedosto. Firestore sallii pyynnön, jos jokin täsmäävä sääntö sallii sen.
match /orders/{id} {
allow read: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
}
match /{document=**} {
allow read: if request.auth != null;
}
Ensimmäinen lohko näyttää hyvältä omistajasäännöltä. Toisen lohkon ollessa mukana A luki B:n tilauksen 200:lla, ulkopuolinen listasi kaikki 3 tilausta ja sama ulkopuolinen luki A:n dokumentin kokoelmasta users. Ensimmäisessä lohkossa mikään ei varoita. Poista laaja match; ei ole keinoa saada myöhempää sääntöä ottamaan pääsy takaisin.
Miten käyttäjä tekee itsestään adminin?
Tämä malli tulee vastaan aina, kun roolit asuvat dokumentissa:
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();
}
“Käyttäjät voivat muokata omaa profiiliaan” ja “adminit voivat lukea kaikki tilaukset” ovat kumpikin järkeviä. Yhdessä ne ovat oikeuksien korotus.
Korjaus on estää asiakasta kirjoittamasta tuota kenttää. Kun kirjoitussääntöön lisättiin request.resource.data.role == resource.data.role, sama pyyntö palautti 403 ja A pysyi ulkona B:n tilauksesta. Palvelimelta asetetut custom claims ovat toinen tie, koska asiakas ei voi muuttaa niitä.
Kaksi asiaa, jotka osoittautuivat kunnossa oleviksi
Ylätason säännöt eivät vuoda alikokoelmiin. Kun /orders/{id} oli allow read kaikille kirjautuneille, A luki tilauksen, ja sen alla oleva /orders/b1/notes/n1 palautti 403. Se on dokumentoitu käytös: säännöt pätevät vain täsmäävällä polulla.
Vieras ei myöskään lipsahda läpi vertailussa puuttuvaan tokeniin. resource.data.ownerId == request.auth.uid palautti vieraalle 403 ilman erillistä request.auth != null -ehtoakin. Kirjoitan tarkistuksen silti auki, koska silloin sääntö sanoo sen, mitä tarkoittaa.
Miten omat sääntösi tarkistetaan?
Aja pyyntösi emulaattorissa vieraana, ulkopuolisena ja toisena käyttäjänä. Se paljasti jokaisen yllä olevan tapauksen; sääntöjen lukeminen ei.
Ensimmäiselle läpikäynnille ilmainen Firebase-sääntötarkistus lukee liitetyt säännöt koskematta projektiisi. Tässä käytetyistä sääntöjoukoista se liputti testitilan, vain kirjautumista vaativan pääsyn, laajan matchin ja itse kirjoitetun roolin. Se lukee vain tekstiä, joten se ei voi kertoa, millaista datasi on tai onko sääntö julkaistu.
apiKey sivullasi ei ole yksinään ongelma. Firebasen web-konfiguraatio on suunnitellusti julkinen, ja ilmainen secret scanner merkitsee sen niin, koska sen takana olevaa dataa suojaavat säännöt. Tiedostoilla on taas omat sääntönsä ja vuoto, jota säännöt eivät pysäytä, ja sen testasin artikkelissa Firebase Storagen säännöt.
Rajoitukset. Vain Firestore, paikallisessa emulaattorissa, REST:in yli, keksityllä datalla. Realtime Database käyttää toista sääntökieltä, eikä sitä testattu. App Check, joka rajoittaa, mitkä sovellukset saavat kutsua backendiäsi, ei kuulu tähän; se ei korvaa sääntöjä. Tuotanto voi poiketa emulaattorista yksityiskohdissa, joita en tarkistanut.
Etkö ole varma, mitä sääntösi päästävät läpi?
Luen ne mielelläni kanssasi. Ei myyntipuhetta.
Ota yhteyttä