En resumen
Envié las mismas peticiones contra 11 conjuntos de reglas de Firestore como invitado, como usuario anónimo y como tres usuarios con sesión iniciada. request.auth != null dejó que un desconocido leyera, cambiara y borrara los pedidos de todos. Una comprobación de propietario solo sobre resource dejó que un usuario le pasara un pedido a otra persona. Un {document=**} demasiado amplio abrió una colección que tenía una regla de propietario correcta. Y un rol guardado en un documento que el usuario puede escribir lo convirtió en admin con una sola petición. Cada caso tiene abajo su arreglo, y la regla corregida aguantó las mismas peticiones.
Una app de Firebase no tiene ningún servidor suyo entre el navegador y la base de datos. La configuración del proyecto viaja en la página, y las reglas de seguridad son lo único que separa sus datos de cualquiera que la tenga. Por eso quería ejemplos de reglas con el ataque al lado, no una lista de reglas que parecen correctas.
Cómo probé. Firebase Local Emulator Suite de Firebase CLI 15.34.2, el 3 de noviembre de 2026. Cargué cada conjunto de reglas, reinicié los datos y llamé a la API REST de Firestore con tokens reales del emulador de Auth: sin token, un usuario anónimo, los usuarios A y B, y un desconocido que solo se había registrado. Los datos eran inventados: los pedidos a1 y a2 de A, y b1 de B.
¿Qué reglas de Firebase tienen fugas?
| Regla | Petición | Resultado |
|---|---|---|
if true (modo de prueba) |
Un invitado lista los pedidos | 200, los 3 |
if true (modo de prueba) |
Un invitado borra el pedido de B | 200 |
request.auth != null |
Un usuario anónimo lista los pedidos | 200, los 3 |
request.auth != null |
Un desconocido borra el pedido de A | 200 |
Comprobación de propietario solo en resource |
A crea un pedido cuyo dueño es B | 200 |
Comprobación de propietario solo en resource |
A cambia el ownerId de su pedido |
200 |
Regla de propietario más lectura {document=**} |
A lee el pedido de B | 200 |
El usuario puede escribir su propio doc users |
A pone role: admin y lee el pedido de B |
200 |
¿Qué abre realmente el modo de prueba?
match /{document=**} {
allow read, write: if true;
}
Todo, a todo el mundo. Un invitado sin token listó los 3 pedidos, borró uno y creó uno nuevo. Nada de esto necesita una cuenta ni un secreto filtrado, solo el id del proyecto.
La versión con fecha que suele usar el modo de prueba falla de otra manera. Con if request.time < timestamp.date(2026, 1, 1) y esa fecha ya pasada, el invitado recibió 403 y el usuario A también al leer su propio pedido. La fuga se acabó, y la app también.
¿Basta con request.auth != null?
No. Solo dice que detrás de la petición hay alguna cuenta.
match /orders/{id} {
allow read, write: if request.auth != null;
}
El invitado recibió 403, y por eso esta regla parece segura en una prueba rápida. Después, un desconocido que solo se había registrado listó los 3 pedidos, cambió el total del pedido de B y borró el pedido de A, todo con 200. Un usuario anónimo también listó los 3. Si el registro está abierto, esta regla significa “todo el que se moleste en registrarse”. La documentación de Firebase también la incluye entre los patrones inseguros habituales.
¿Por qué la comprobación de propietario necesita resource y request.resource?
resource es el documento tal como está guardado. request.resource es el documento como quedaría tras la escritura. Una regla que mira solo el primero comprueba de quién es la fila ahora, y no dice nada de en qué la convierte el usuario.
match /orders/{id} {
allow read, update, delete: if resource.data.ownerId == request.auth.uid;
allow create: if request.auth != null;
}
Las lecturas se veían bien: A recibió 200 en su pedido y 403 en el de B. Las escrituras no. A creó un pedido con ownerId de B y recibió 200. Luego A cambió el ownerId de su propio pedido a B, también 200, y desde ese momento B podía leerlo. En una app donde los pedidos son facturas o saldos, es un usuario escribiendo en la cuenta de otro.
Esta versión rechazó todo:
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;
}
| Petición de A | Resultado |
|---|---|
| Leer su pedido | 200 |
| Leer, actualizar, borrar el pedido de B | 403 |
| Crear un pedido cuyo dueño es B | 403 |
Cambiar ownerId a B |
403 |
Aun así dejó que A cambiara el total de su propio pedido, con 200. Si el cliente no debe poder fijar ese campo, diga qué campos puede cambiar:
allow update: if request.auth != null
&& resource.data.ownerId == request.auth.uid
&& request.resource.data.diff(resource.data).affectedKeys().hasOnly(['note']);
Con esto, cambiar total devolvió 403 y cambiar note devolvió 200.
¿Por qué falló mi consulta con una regla correcta?
Porque las reglas no son filtros. Con la regla de propietario de arriba, A pidiendo toda la colección recibió 403, aunque A es dueño de 2 de los 3 pedidos. Firestore juzga la consulta por lo que podría devolver, no por lo que hay.
| Petición de A | Resultado |
|---|---|
| Listar todos los pedidos | 403 |
Consulta ownerId == A |
200, 2 docs |
Consulta ownerId == B |
403 |
Por eso, cuando una lista se rompe tras endurecer las reglas, hay que arreglar la consulta. Aflojar la regla para que la lista funcione es como se escriben las reglas demasiado amplias de arriba.
¿Puede una regla amplia anular una correcta?
Sí, y es la que buscaría primero en un proyecto con un archivo de reglas largo. Firestore permite una petición si cualquier regla que coincida la permite.
match /orders/{id} {
allow read: if request.auth != null
&& resource.data.ownerId == request.auth.uid;
}
match /{document=**} {
allow read: if request.auth != null;
}
El primer bloque parece una buena regla de propietario. Con el segundo presente, A leyó el pedido de B con 200, un desconocido listó los 3 pedidos, y ese mismo desconocido leyó el documento de A en users. Nada en el primer bloque avisa. Borre la regla amplia; no hay forma de que una regla posterior quite el acceso.
¿Cómo se hace admin un usuario?
Este patrón aparece siempre que los roles viven en un documento:
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();
}
“Los usuarios pueden editar su propio perfil” y “los admins pueden leer todos los pedidos” son razonables por separado. Juntas son una escalada de privilegios.
El arreglo es impedir que el cliente escriba ese campo. Con request.resource.data.role == resource.data.role añadido a la regla de escritura, la misma petición devolvió 403 y A siguió fuera del pedido de B. Los custom claims fijados desde un servidor son la otra vía, porque un cliente no puede cambiarlos.
Dos cosas que resultaron estar bien
Las reglas de un documento padre no se filtran a las subcolecciones. Con allow read en /orders/{id} para cualquier usuario con sesión, A leyó el pedido, y /orders/b1/notes/n1 debajo devolvió 403. Es el comportamiento documentado: las reglas se aplican solo en la ruta coincidente.
Un invitado tampoco se cuela con una comparación contra un token ausente. resource.data.ownerId == request.auth.uid devolvió 403 para el invitado incluso sin un request.auth != null aparte. Aun así escribo la comprobación, porque así la regla dice lo que quiere decir.
¿Cómo comprobar sus propias reglas?
Ejecute sus peticiones en el emulador como invitado, como desconocido y como un segundo usuario. Eso fue lo que expuso cada caso de arriba; leer las reglas no.
Para una primera pasada, el revisor de reglas de Firebase gratuito lee reglas pegadas sin tocar su proyecto. En los conjuntos de aquí señaló el modo de prueba, el acceso solo con sesión, la regla amplia y el rol que escribe el propio usuario. Lee solo texto, así que no puede decirle cómo son sus datos ni si una regla llegó a desplegarse.
Su apiKey en la página no es el problema por sí sola. La configuración web de Firebase es pública por diseño, y el escáner de secretos gratuito la marca así, porque lo que protege los datos detrás son las reglas. Los archivos siguen otras reglas, con una fuga que las reglas no pueden detener, que probé en reglas de Firebase Storage.
Límites. Solo Firestore, en el emulador local, por REST, con datos inventados. La Realtime Database usa otro lenguaje de reglas y no se probó. App Check, que limita qué apps pueden llamar a su backend, no entra aquí; no sustituye a las reglas. La producción puede diferir del emulador en detalles que no comprobé.