Cómo funcionan las reglas de seguridad

La seguridad puede ser uno de los factores más complejos en el proceso de desarrollo de las apps. En la mayoría de las aplicaciones, los programadores tienen que construir y poner en funcionamiento un servidor que se ocupe de la autenticación (determinar la identidad del usuario) y la autorización (controlar qué puede hacer cada usuario).

Las reglas de seguridad de Firebase quitan la capa intermedia (servidor) y te permiten especificar permisos basados en rutas de acceso para los clientes que se conectan directamente a tus datos. Usa esta guía para obtener más información sobre cómo se aplican las reglas a las solicitudes entrantes.

Selecciona un producto para obtener más información sobre sus reglas.

Cloud Firestore

Estructura básica

Las reglas de seguridad de Firebase en Cloud Firestore y Storage utilizan la siguiente estructura y sintaxis:

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

Es importante comprender los siguientes conceptos clave a medida que compilas las reglas:

  • Solicitud: El método o los métodos invocados en la declaración allow que permites que se ejecuten. Los métodos estándar son get, list, create, update y delete. Los métodos de conveniencia read y write permiten un amplio acceso de lectura y escritura en la base de datos o ruta de almacenamiento especificada.
  • Ruta de acceso: La ubicación de la base de datos o del almacenamiento, representada como una ruta URI.
  • Regla: Es la declaración allow, que incluye una condición que permite una solicitud si se evalúa como verdadera.

Reglas de seguridad versión 2

A partir de mayo de 2019, la versión 2 de las reglas de seguridad de Firebase ya está disponible. En esta versión, cambia el comportamiento de los comodines recurrentes {name=**}. Debes usar la versión 2 si deseas usar las consultas de grupos de colecciones. Para usar la versión 2, debes poner rules_version = '2'; en la primera línea de tus reglas de seguridad:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

Rutas de acceso coincidentes

Todas las declaraciones de coincidencia deben apuntar a los documentos, no a las colecciones. Una declaración de coincidencia puede apuntar a un documento específico, como en match /cities/SF, o usar comodines para apuntar a cualquier documento en la ruta especificada, como en match /cities/{city}.

En el ejemplo anterior, la declaración de coincidencia usa la sintaxis de comodín {city}. Esto significa que la regla se aplica a cualquier documento de la colección cities, como /cities/SF o /cities/NYC. Cuando se evalúan las expresiones allow en la declaración de coincidencia, la variable city se convierte en el nombre del documento de la ciudad, como SF o NYC.

Subcolecciones coincidentes

Los datos de Cloud Firestore se organizan en colecciones de documentos y cada documento puede extender la jerarquía a través de subcolecciones. Es importante comprender cómo interactúan las reglas de seguridad con los datos jerárquicos.

Considera la situación en la que cada documento de la colección cities contiene una subcolección landmarks. Las reglas de seguridad se aplican solo a la ruta que coincide, por lo que no se aplican los controles de acceso definidos de la colección cities a la subcolección landmarks. En cambio, debes escribir reglas explícitas para controlar el acceso a las subcolecciones:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      allow read, write: if <condition>;

        // Explicitly define rules for the 'landmarks' subcollection
        match /landmarks/{landmark} {
          allow read, write: if <condition>;
        }
    }
  }
}

Cuando se anidan declaraciones match, la ruta de la declaración match interna siempre está relacionada con la ruta de la declaración match externa. Por lo tanto, los siguientes conjuntos de reglas son equivalentes:

service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city} {
      match /landmarks/{landmark} {
        allow read, write: if <condition>;
      }
    }
  }
}
service cloud.firestore {
  match /databases/{database}/documents {
    match /cities/{city}/landmarks/{landmark} {
      allow read, write: if <condition>;
    }
  }
}

Comodines recurrentes

Si quieres usar reglas que se apliquen a una jerarquía de profundidad arbitraria, usa la sintaxis de comodines recurrentes, {name=**}, como se muestra a continuación:

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

Cuando uses esta sintaxis, la variable de comodines contendrá el segmento de la ruta que coincide completo, incluso si el documento se ubica en una subcolección anidada a mayor profundidad. Por ejemplo, las reglas anteriores coincidirían con un documento ubicado en /cities/SF/landmarks/coit_tower y el valor de la variable document sería SF/landmarks/coit_tower.

Sin embargo, ten en cuenta que el comportamiento de los comodines recurrentes depende de la versión de las reglas.

Versión 1

Las reglas de seguridad usan la versión 1 de forma predeterminada. En la versión 1, los comodines recurrentes coinciden con uno o más elementos de ruta. No obstante, no pueden coincidir con una ruta vacía, por lo que match /cities/{city}/{document=**} coincide con los documentos de las subcolecciones, pero no con los de la colección cities, mientras que match /cities/{document=**} coincide con los documentos en la colección cities y sus subcolecciones.

Los comodines recurrentes deben aparecer al final de una declaración match.

Version 2

En la versión 2 de las reglas de seguridad, los comodines recurrentes coinciden con cero o más elementos de ruta, por lo que match/cities/{city}/{document=**} coincide con los documentos de cualquier subcolección o colección de cities.

Para usar la versión 2, debes agregar rules_version = '2'; en la parte superior de tus reglas de seguridad:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the cities collection as well as any document
    // in a subcollection.
    match /cities/{city}/{document=**} {
      allow read, write: if <condition>;
    }
  }
}

Puedes tener, como máximo, un comodín recurrente por declaración match, pero, en la versión 2, puedes colocar este comodín en cualquier lugar de la declaración. Por ejemplo:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the songs collection group
    match /{path=**}/songs/{song} {
      allow read, write: if <condition>;
    }
  }
}

Debes usar la versión 2 si deseas usar las consultas de grupos de colecciones; consulta cómo proteger las consultas de grupos de colecciones.

Superposiciona declaraciones match

Es posible que un documento coincida con más de una declaración match. Si varias expresiones allow coinciden con una solicitud, se permite el acceso siempre que alguna de las condiciones sea true:

service cloud.firestore {
  match /databases/{database}/documents {
    // Matches any document in the 'cities' collection.
    match /cities/{city} {
      allow read, write: if false;
    }

    // Matches any document in the 'cities' collection or subcollections.
    match /cities/{document=**} {
      allow read, write: if true;
    }
  }
}

En el ejemplo anterior, se permitirán todas las lecturas y escrituras en la colección cities, ya que la segunda regla siempre es true, incluso si la primera regla siempre es false.

Límites de las reglas de seguridad

Cuando trabajes con reglas de seguridad, ten en cuenta los siguientes límites:

Límite Detalles
Cantidad máxima de llamadas exists(), get() y getAfter() por solicitud
  • 10 para las solicitudes de un solo documento o las solicitudes de consulta
  • 20 para las lecturas de varios documentos, transacciones y escrituras en lotes. El límite anterior de 10 también se aplica a cada operación

    Por ejemplo, imagina que creas una solicitud de escritura en lotes con 3 operaciones de escritura y que tus reglas de seguridad usan 2 llamadas de acceso a documentos para validar cada escritura. En este caso, cada escritura utiliza 2 de sus 10 llamadas de acceso y la solicitud de escritura en lotes utiliza 6 de sus 20 llamadas de acceso.

Si se excede alguno de esos límites, se obtiene un error de permiso denegado.

Algunas llamadas de acceso a documentos se pueden almacenar en caché, y estas llamadas no cuentan para los límites.

Profundidad máxima de las declaraciones match anidadas 10
Longitud máxima de la ruta de acceso, en segmentos de ruta, permitida en un conjunto de declaraciones match anidadas: 100
Cantidad máxima de variables de captura de rutas de acceso permitida en un conjunto de instrucciones match anidadas 20
Profundidad máxima de las llamadas a funciones 20
Cantidad máxima de argumentos de funciones 7
Cantidad máxima de vinculaciones a variables let por función 10
Cantidad máxima de llamadas recurrentes o cíclicas a una función 0 (no permitidas)
Cantidad máxima de expresiones evaluadas por solicitud 1,000
Tamaño máximo de un conjunto de reglas Los conjuntos de reglas de Verax deben cumplir con dos límites de tamaño:
  • 256 KB en el tamaño del texto origen del conjunto de reglas de Verax publicado desde Firebase console o la CLI con firebase deploy
  • 250 KB en el tamaño del conjunto de reglas compilado que se genera cuando Firebase procesa la fuente de Verax y la activa en el backend

Cloud Storage

Estructura básica

Las reglas de seguridad de Firebase en Cloud Firestore y Storage utilizan la siguiente estructura y sintaxis:

service <<name>> {
  // Match the resource path.
  match <<path>> {
    // Allow the request if the following conditions are true.
    allow <<methods>> : if <<condition>>
  }
}

Es importante comprender los siguientes conceptos clave a medida que compilas las reglas:

  • Solicitud: El método o los métodos invocados en la declaración allow que permites que se ejecuten. Los métodos estándar son get, list, create, update y delete. Los métodos de conveniencia read y write permiten un amplio acceso de lectura y escritura en la base de datos o ruta de almacenamiento especificada.
  • Ruta de acceso: La ubicación de la base de datos o del almacenamiento, representada como una ruta URI.
  • Regla: Es la declaración allow, que incluye una condición que permite una solicitud si se evalúa como verdadera.

Rutas de acceso coincidentes

Las reglas de seguridad de Storage usan match en las rutas de acceso de los archivos en Cloud Storage. Las reglas pueden usar match en las rutas de acceso exactas o comodines, y también pueden anidarse. Si ninguna regla de coincidencia permite un método de solicitud o la condición se evalúa como false, la solicitud se rechaza.

Coincidencias exactas

// Exact match for "images/profilePhoto.png"
match /images/profilePhoto.png {
  allow write: if <condition>;
}

// Exact match for "images/croppedProfilePhoto.png"
match /images/croppedProfilePhoto.png {
  allow write: if <other_condition>;
}

Coincidencias anidadas

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/profilePhoto.png"
  match /profilePhoto.png {
    allow write: if <condition>;
  }

  // Exact match for "images/croppedProfilePhoto.png"
  match /croppedProfilePhoto.png {
    allow write: if <other_condition>;
  }
}

Coincidencias con comodín

Las reglas también se pueden usar para aplicar match a un patrón con comodines. Un comodín es una variable con nombre que representa una sola string, como profilePhoto.png, o varios segmentos de ruta, como images/profilePhoto.png.

Para crear un comodín, se encierra el nombre del comodín entre llaves (p. ej., {string}). Para declarar un comodín de varios segmentos, se agrega =** al nombre del comodín, como {path=**}:

// Partial match for files that start with "images"
match /images {
  // Exact match for "images/*"
  // e.g. images/profilePhoto.png is matched
  match /{imageId} {
    // This rule only matches a single path segment (*)
    // imageId is a string that contains the specific segment matched
    allow read: if <condition>;
  }

  // Exact match for "images/**"
  // e.g. images/users/user:12345/profilePhoto.png is matched
  // images/profilePhoto.png is also matched!
  match /{allImages=**} {
    // This rule matches one or more path segments (**)
    // allImages is a path that contains all segments matched
    allow read: if <other_condition>;
  }
}

Si varias reglas coinciden con un archivo, se usa un operador OR para determinar el resultado de todas las reglas evaluadas. Es decir, si alguna de las reglas con las que coincide el archivo se evalúa como true, el resultado es true.

En las reglas anteriores, se puede leer el archivo "images/profilePhoto.png" si condition o other_condition se evalúan como verdaderos, mientras que el archivo "images/users/user:12345/profilePhoto.png" solo está sujeto al resultado de other_condition.

Se puede hacer referencia a una variable comodín en match para proporcionar autorización de ruta de acceso o nombre de archivo:

// Another way to restrict the name of a file
match /images/{imageId} {
  allow read: if imageId == "profilePhoto.png";
}

Las reglas de seguridad de Storage no se transmiten en cascada y solo se evalúan cuando la ruta de acceso de la solicitud coincide con una ruta de acceso para la que se especificaron reglas.

Evaluación de solicitudes

Las cargas, las descargas, los cambios de metadatos y las eliminaciones se evalúan con el request enviado a Cloud Storage. La variable request contiene la ruta de acceso al archivo en la que se realiza la solicitud, el momento en que se recibe la solicitud y el valor resource nuevo, si la solicitud es una escritura. También se incluyen el estado de autenticación y los encabezados HTTP.

El objeto request también contiene el ID único del usuario y la carga útil de Firebase Authentication en el objeto request.auth, que se explicará con más detalle en la sección Seguridad basada en el usuario de la documentación.

A continuación, se muestra una lista completa de las propiedades del objeto request:

Propiedad Tipo Descripción
auth mapa <string, string> Cuando un usuario accede, proporciona el uid, el ID único del usuario y token, un mapa de las reclamaciones de JWT de Firebase Authentication. De lo contrario, será null.
params mapa <string, string> Un mapa que contiene los parámetros de consulta de la solicitud.
path ruta Un objeto path que representa la ruta en la que se realiza la solicitud.
resource mapa <string, string> El nuevo valor del recurso, presente solo en solicitudes de tipo write.
time marca de tiempo Una marca de tiempo que representa la hora del servidor a la que se evalúa la solicitud.

Evaluación de recursos

Para la evaluación de reglas, te recomendamos que también evalúes los metadatos del archivo que se va a subir, descargar, modificar o borrar. Esto te permite crear reglas complejas y potentes que ejecutan tareas como permitir que se suban archivos con ciertos tipos de contenido solamente o que solo se borren los archivos que superen cierto tamaño.

Las reglas de seguridad de Firebase para Cloud Storage proporcionan metadatos de archivo en el objeto resource, que contiene pares clave-valor de los metadatos que se muestran en un objeto de Cloud Storage. Estas propiedades se pueden inspeccionar en solicitudes read o write para garantizar la integridad de los datos.

En las solicitudes write (como cargas, actualizaciones de metadatos y eliminaciones), además del objeto resource, que contiene metadatos de archivo para el archivo que existe actualmente en la ruta de la solicitud, también puedes usar el objeto request.resource, que contiene un subconjunto de los metadatos del archivo que se escribirán si se permite la escritura. Puedes usar estos dos valores para garantizar la integridad de los datos o hacer cumplir las restricciones de la aplicación, como el tipo o el tamaño de los archivos.

A continuación, se muestra una lista completa de las propiedades del objeto resource:

Propiedad Tipo Descripción
name string El nombre completo del objeto.
bucket string El nombre del depósito en el que reside el objeto.
generation int La generación de objeto GCS de este objeto.
metageneration int La metageneración de objeto GCS de este objeto.
size int El tamaño del objeto en bytes.
timeCreated marca de tiempo Una marca de tiempo que representa la hora a la que se creó el objeto.
updated marca de tiempo Una marca de tiempo que representa la hora a la que se actualizó el objeto por última vez.
md5Hash string Un hash MD5 del objeto.
crc32c string Un hash crc32c del objeto.
etag string La etag asociada con este objeto.
contentDisposition string La disposición de contenido asociada con este objeto.
contentEncoding string La codificación de contenido asociada con este objeto.
contentLanguage string El idioma del contenido asociado con este objeto.
contentType string El tipo de contenido asociado con este objeto.
metadata mapa <string, string> Pares clave-valor de los metadatos personalizados adicionales que especifica el desarrollador.

request.resource contiene todas ellas, excepto generation, metageneration, etag, timeCreated y updated.

Ejemplo completo

A modo de revisión general, puedes crear un ejemplo completo de reglas para una solución de almacenamiento de imágenes:

service firebase.storage {
 match /b/{bucket}/o {
   match /images {
     // Cascade read to any image type at any path
     match /{allImages=**} {
       allow read;
     }

     // Allow write files to the path "images/*", subject to the constraints:
     // 1) File is less than 5MB
     // 2) Content type is an image
     // 3) Uploaded content type matches existing content type
     // 4) File name (stored in imageId wildcard variable) is less than 32 characters
     match /{imageId} {
       allow write: if request.resource.size < 5 * 1024 * 1024
                    && request.resource.contentType.matches('image/.*')
                    && request.resource.contentType == resource.contentType
                    && imageId.size() < 32
     }
   }
 }
}

Realtime Database

Estructura básica

En Realtime Database, las reglas de seguridad de Firebase son expresiones similares a las de JavaScript, contenidas en un documento JSON.

Usan la siguiente sintaxis:

{
  "rules": {
    "<<path>>": {
    // Allow the request if the condition for each method is true.
      ".read": <<condition>>,
      ".write": <<condition>>,
      ".validate": <<condition>>
    }
  }
}

Hay tres elementos básicos en la regla:

  • Ruta: La ubicación de la base de datos. Esto duplica la estructura JSON de tu base de datos.
  • Solicitud: Estos son los métodos que usa la regla para otorgar acceso. Las reglas read y write otorgan acceso amplio de lectura y escritura, mientras que las reglas validate actúan como verificación secundaria para otorgar acceso según los datos entrantes o existentes.
  • Condición: Es la condición que permite una solicitud si se evalúa como verdadera.

Cómo se aplican las reglas a las rutas de acceso

En Realtime Database, las reglas se aplican de forma atómica, lo que significa que las reglas en los nodos primarios de nivel superior anulan las reglas en los nodos secundarios más detallados, y las reglas, en un nodo más detallado, no pueden otorgar acceso a una ruta de acceso primaria. No puedes definir mejor el acceso, o revocarlo en una ruta más detallada en la estructura de tu base de datos, si ya lo otorgaste en una de las rutas de acceso primarias.

Ten en cuenta las siguientes reglas:

{
  "rules": {
     "foo": {
        // allows read to /foo/*
        ".read": "data.child('baz').val() === true",
        "bar": {
          // ignored, since read was allowed already
          ".read": false
        }
     }
  }
}

Esta estructura de seguridad permite leer /bar/ cada vez que /foo/ contiene un elemento secundario baz con el valor true. La regla ".read": false en /foo/bar/ no tiene efecto aquí, ya que una ruta secundaria no puede revocar el acceso.

Aunque no parezca intuitivo inmediatamente, esta es una parte poderosa del lenguaje de reglas y permite la implementación de privilegios de acceso complejos con un esfuerzo mínimo. Esto es particularmente útil para la seguridad basada en usuarios.

Sin embargo, las reglas .validate no se propagan en cascada. Para que se autorice una operación de escritura, deben cumplirse todas las reglas de validación en todos los niveles de la jerarquía.

Además, debido a que las reglas no se aplican a una ruta de acceso primaria, la operación de lectura o escritura fallará si no hay una regla en la ubicación solicitada o en una ubicación primaria que otorga acceso. Incluso cuando sea posible acceder a todas las rutas de acceso secundarias afectadas, la lectura en la ubicación primaria fallará por completo. Ten en cuenta esta estructura:

{
  "rules": {
    "records": {
      "rec1": {
        ".read": true
      },
      "rec2": {
        ".read": false
      }
    }
  }
}

Sin comprender que las reglas se evalúan de forma atómica, puede parecer que recuperar la ruta /records/ mostraría rec1, pero no rec2. Sin embargo, el resultado real es un error:

JavaScript
var db = firebase.database();
db.ref("records").once("value", function(snap) {
  // success method is not called
}, function(err) {
  // error callback triggered with PERMISSION_DENIED
});
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[_ref child:@"records"] observeSingleEventOfType:FIRDataEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
  // success block is not called
} withCancelBlock:^(NSError * _Nonnull error) {
  // cancel block triggered with PERMISSION_DENIED
}];
Swift
var ref = FIRDatabase.database().reference()
ref.child("records").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // success block is not called
}, withCancelBlock: { error in
    // cancel block triggered with PERMISSION_DENIED
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // success method is not called
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback triggered with PERMISSION_DENIED
  });
});
REST
curl https://docs-examples.firebaseio.com/rest/records/
# response returns a PERMISSION_DENIED error

Dado que la operación de lectura en /records/ es atómica y no hay una regla de lectura que otorgue acceso a todos los datos en /records/, esto mostrará un error PERMISSION_DENIED. Si evaluamos esta regla en el simulador de seguridad de Firebase console, se puede ver que la operación de lectura se rechazó:

Attempt to read /records with auth=Success(null)
    /
    /records

No .read rule allowed the operation.
Read was denied.

Se rechazó la operación porque ninguna regla de lectura permitió el acceso a la ruta /records/, pero ten en cuenta que la regla para rec1 nunca se evaluó porque no estaba en la ruta solicitada. Para recuperar rec1, tendríamos que acceder a él directamente:

JavaScript
var db = firebase.database();
db.ref("records/rec1").once("value", function(snap) {
  // SUCCESS!
}, function(err) {
  // error callback is not called
});
Objective-C
FIRDatabaseReference *ref = [[FIRDatabase database] reference];
[[ref child:@"records/rec1"] observeSingleEventOfType:FEventTypeValue withBlock:^(FIRDataSnapshot *snapshot) {
    // SUCCESS!
}];
Swift
var ref = FIRDatabase.database().reference()
ref.child("records/rec1").observeSingleEventOfType(.Value, withBlock: { snapshot in
    // SUCCESS!
})
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("records/rec1");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
  @Override
  public void onDataChange(DataSnapshot snapshot) {
    // SUCCESS!
  }

  @Override
  public void onCancelled(FirebaseError firebaseError) {
    // error callback is not called
  }
});
REST
curl https://docs-examples.firebaseio.com/rest/records/rec1
# SUCCESS!

Variable de ubicación

Las reglas de Realtime Database admiten una variable $location que coincida con segmentos de la ruta de acceso. Usa el prefijo $ delante del segmento de la ruta de acceso para hacer coincidir tu regla con los nodos secundarios en la ruta.

  {
    "rules": {
      "rooms": {
        // This rule applies to any child of /rooms/, the key for each room id
        // is stored inside $room_id variable for reference
        "$room_id": {
          "topic": {
            // The room's topic can be changed if the room id has "public" in it
            ".write": "$room_id.contains('public')"
          }
        }
      }
    }
  }

También puedes usar $variable en paralelo con nombres de ruta de acceso constantes.

  {
    "rules": {
      "widget": {
        // a widget can have a title or color attribute
        "title": { ".validate": true },
        "color": { ".validate": true },

        // but no other child paths are allowed
        // in this case, $other means any key excluding "title" and "color"
        "$other": { ".validate": false }
      }
    }
  }