Cómo proteger tus datos

Las reglas de Firebase Database son una configuración declarativa para tu base de datos. Esto significa que las reglas se definen de manera independiente de la lógica del producto. Esto tiene varias ventajas: los clientes no son responsables de aplicar las condiciones de seguridad, las implementaciones erróneas no afectan tus datos y, lo que tal vez es más importante, no se necesita una referencia intermedia, como un servidor, para proteger los datos del mundo.

Cómo estructurar tus reglas

Las reglas de Firebase Database son expresiones similares a las de Javascript, contenidas en un documento JSON. La estructura de tus reglas debe seguir la de los datos que almacenaste en tu base de datos. Por ejemplo, supongamos que haces un seguimiento de una lista de mensajes y tienes datos que lucen de la siguiente forma:

{
  "messages": {
    "message0": {
      "content": "Hello",
      "timestamp": 1405704370369
    },
    "message1": {
      "content": "Goodbye",
      "timestamp": 1405704395231
    },
    ...
  }
}

Tus reglas deben estructurarse de manera similar. A continuación, aparece un conjunto de reglas de ejemplo que podría servir para esta estructura de datos.

{
  "rules": {
    "messages": {
      "$message": {
        // only messages from the last ten minutes can be read
        ".read": "data.child('timestamp').val() > (now - 600000)",

        // new messages must have a string content and a number timestamp
        ".validate": "newData.hasChildren(['content', 'timestamp']) && newData.child('content').isString() && newData.child('timestamp').isNumber()"
      }
    }
  }
}

Tipos de reglas de seguridad

Existen tres tipos de reglas para aplicar las condiciones de seguridad: .write, .read, y .validate. A continuación, se incluye un breve resumen de sus propósitos:

Tipos de reglas
.read Describe si los usuarios pueden leer los datos y cuándo pueden hacerlo.
.write Indica si se permite la escritura de datos y en qué momento se permite.
.validate Define el aspecto de un valor con formato correcto, si este tiene atributos secundarios y el tipo de datos.

Variables predefinidas

Hay un número de variables predefinidas útiles a las que se puede acceder dentro de una definición de reglas de seguridad. Se usará la mayoría de ellas en los siguientes ejemplos. A continuación, se incluye un breve resumen de cada una y un enlace a la referencia de la API correspondiente.

Variables predefinidas
now La hora actual en milisegundos desde el epoch Linux. Esto funciona particularmente bien para validar marcas de tiempo creadas con firebase.database.ServerValue.TIMESTAMP del SDK.
root Una RuleDataSnapshot que representa la ruta de acceso de raíz en la base de datos de Firebase, tal como existe antes de la operación que se intenta ejecutar.
newData Una RuleDataSnapshot que representa los datos como existirían después de la operación que se intenta ejecutar. Incluye los datos nuevos que se escriben y los datos existentes.
data Una RuleDataSnapshot que representa los datos como existían antes de la operación que se intenta ejecutar.
$ variables Ruta de acceso de comodín que se usa para representar los ID y las claves secundarias dinámicas.
auth Representa la carga útil del token de un usuario autenticado.

Estas variables pueden usarse en cualquier sección de tus reglas. Por ejemplo, las reglas de seguridad que se muestran a continuación garantizan que los datos escritos en el nodo /foo/ deben ser una string con menos de 100 caracteres:

{
  "rules": {
    "foo": {
      // /foo is readable by the world
      ".read": true,

      // /foo is writable by the world
      ".write": true,

      // data written to /foo must be a string less than 100 characters
      ".validate": "newData.isString() && newData.val().length < 100"
    }
  }
}

Datos existentes en comparación con datos nuevos

La variable data predefinida se usa para hacer referencia a los datos antes de que se ejecute una operación de escritura. Por el contrario, la variable newData contiene los datos nuevos que existirán si la operación de escritura se ejecuta correctamente. newData representa el resultado combinado de los datos nuevos que se escriben y los datos existentes.

A modo de ejemplo, esta regla nos permitirá crear registros nuevos o borrar los existentes, pero no podremos realizar cambios en los datos existentes que no son nulos:

// we can write as long as old data or new data does not exist
// in other words, if this is a delete or a create, but not an update
".write": "!data.exists() || !newData.exists()"

Referencia a datos en otras rutas

Se puede usar cualquier dato como criterio para las reglas. Podemos usar las variables predefinidas root, data y newData para acceder a cualquier ruta de acceso como existiría antes o después de un evento de escritura.

Considera el siguiente ejemplo, que permite operaciones de escritura siempre que el valor del nodo /allow_writes/ sea true, el nodo superior no tenga establecida una marca readOnly y exista un elemento secundario llamado foo en los datos nuevos que se escriben:

".write": "root.child('allow_writes').val() === true &&
          !data.parent().child('readOnly').exists() &&
          newData.child('foo').exists()"

Transmisión en cascada de reglas de lectura y escritura

Las reglas .read y .write funcionan desde arriba hacia abajo y las reglas más superficiales anulan las más profundas. Si una regla otorga permisos de lectura o de escritura a una ruta de acceso en particular, también otorga acceso a todas las reglas secundarias debajo de ella. Considera la siguiente estructura:

{
  "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 desde /bar/ siempre que /foo/ contenga un baz secundario con el valor true. La regla ".read": false en /foo/bar/ no tiene efecto en este caso, ya que una ruta de acceso 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. Mostraremos ejemplos de esto cuando abordemos la seguridad basada en usuarios más adelante en esta guía.

Ten en cuenta que las reglas .validate no se aplican 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.

Las reglas no son filtros

Las reglas se aplican de una manera atómica. Esto significa que una operación de lectura o de escritura falla inmediatamente si no hay una regla en dicha ubicación o en una ubicación superior que otorgue acceso. Incluso cuando sea posible acceder a todas las rutas de acceso 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 manera atómica, podría parecer que con la obtención de la ruta de acceso /records/ se mostraría rec1, pero no rec2. El resultado actual, sin embargo, 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 bajo /records/, esto generará un error de PERMISSION_DENIED. Si se evalúa 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.

La operación se rechazó porque ninguna regla de lectura permitió el acceso a la ruta de acceso /records/, pero ten en cuenta que la regla para rec1 nunca se evaluó porque no se encontraba en la ruta de acceso solicitada. Para obtener rec1, debemos acceder de forma directa:

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!

Datos de validación

La aplicación de las estructuras de datos y la validación del formato y del contenido de los datos se deben hacer mediante las reglas de .validate, que solo se ejecutan después de que una regla .write otorga acceso correctamente. A continuación, aparece una definición de muestra de la regla .validate, que solo permite fechas en el formato AAAA-MM-DD entre los años 1900 y 2099, lo que se verifica con una expresión regular.

".validate": "newData.isString() &&
              newData.val().matches(/^(19|20)[0-9][0-9][-\\/. ](0[1-9]|1[012])[-\\/. ](0[1-9]|[12][0-9]|3[01])$/)"

Las reglas .validate son el único tipo de reglas de seguridad que no se transmiten por cascada. Si alguna regla de validación falla en algún registro secundario, se rechazará toda la operación de escritura. Además, las definiciones de validación se ignoran cuando se borran datos (es decir, cuando el valor nuevo que se escribe es null).

Podrían parecer elementos triviales, pero de hecho son funciones importantes en la escritura de reglas sólidas de Firebase Realtime Database. Ten en cuenta las siguientes reglas:

{
  "rules": {
    // write is allowed for all paths
    ".write": true,
    "widget": {
      // a valid widget must have attributes "color" and "size"
      // allows deleting widgets (since .validate is not applied to delete rules)
      ".validate": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99
        ".validate": "newData.isNumber() &&
                      newData.val() >= 0 &&
                      newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical
        // /valid_colors/ index
        ".validate": "root.child('valid_colors/' + newData.val()).exists()"
      }
    }
  }
}

Con esta variante en mente, mira los resultados para las siguientes operaciones de escritura:

JavaScript
var ref = db.ref("/widget");

// PERMISSION_DENIED: does not have children color and size
ref.set('foo');

// PERMISSION DENIED: does not have child color
ref.set({size: 22});

// PERMISSION_DENIED: size is not a number
ref.set({ size: 'foo', color: 'red' });

// SUCCESS (assuming 'blue' appears in our colors list)
ref.set({ size: 21, color: 'blue'});

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child('size').set(99);
Objective-C
FIRDatabaseReference *ref = [[[FIRDatabase database] reference] child: @"widget"];

// PERMISSION_DENIED: does not have children color and size
[ref setValue: @"foo"];

// PERMISSION DENIED: does not have child color
[ref setValue: @{ @"size": @"foo" }];

// PERMISSION_DENIED: size is not a number
[ref setValue: @{ @"size": @"foo", @"color": @"red" }];

// SUCCESS (assuming 'blue' appears in our colors list)
[ref setValue: @{ @"size": @21, @"color": @"blue" }];

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
[[ref child:@"size"] setValue: @99];
Swift
var ref = FIRDatabase.database().reference().child("widget")

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo")

// PERMISSION DENIED: does not have child color
ref.setValue(["size": "foo"])

// PERMISSION_DENIED: size is not a number
ref.setValue(["size": "foo", "color": "red"])

// SUCCESS (assuming 'blue' appears in our colors list)
ref.setValue(["size": 21, "color": "blue"])

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
Java
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference ref = database.getReference("widget");

// PERMISSION_DENIED: does not have children color and size
ref.setValue("foo");

// PERMISSION DENIED: does not have child color
ref.child("size").setValue(22);

// PERMISSION_DENIED: size is not a number
Map<String,Object> map = new HashMap<String, Object>();
map.put("size","foo");
map.put("color","red");
ref.setValue(map);

// SUCCESS (assuming 'blue' appears in our colors list)
map = new HashMap<String, Object>();
map.put("size", 21);
map.put("color","blue");
ref.setValue(map);

// If the record already exists and has a color, this will
// succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
// will fail to validate
ref.child("size").setValue(99);
REST
# PERMISSION_DENIED: does not have children color and size
curl -X PUT -d 'foo' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION DENIED: does not have child color
curl -X PUT -d '{"size": 22}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# PERMISSION_DENIED: size is not a number
curl -X PUT -d '{"size": "foo", "color": "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# SUCCESS (assuming 'blue' appears in our colors list)
curl -X PUT -d '{"size": 21, "color": "blue"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# If the record already exists and has a color, this will
# succeed, otherwise it will fail since newData.hasChildren(['color', 'size'])
# will fail to validate
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Veamos la misma estructura, pero con reglas .write en lugar de .validate:

{
  "rules": {
    // this variant will NOT allow deleting records (since .write would be disallowed)
    "widget": {
      // a widget must have 'color' and 'size' in order to be written to this path
      ".write": "newData.hasChildren(['color', 'size'])",
      "size": {
        // the value of "size" must be a number between 0 and 99, ONLY IF WE WRITE DIRECTLY TO SIZE
        ".write": "newData.isNumber() && newData.val() >= 0 && newData.val() <= 99"
      },
      "color": {
        // the value of "color" must exist as a key in our mythical valid_colors/ index
        // BUT ONLY IF WE WRITE DIRECTLY TO COLOR
        ".write": "root.child('valid_colors/'+newData.val()).exists()"
      }
    }
  }
}

En esta variante, cualquiera de las siguientes operaciones tendría éxito:

JavaScript
var ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.set({size: 99999, color: 'red'});

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child('size').set(99);
Objective-C
Firebase *ref = [[Firebase alloc] initWithUrl:URL];

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
[ref setValue: @{ @"size": @9999, @"color": @"red" }];

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
[[ref childByAppendingPath:@"size"] setValue: @99];
Swift
var ref = Firebase(url:URL)

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
ref.setValue(["size": 9999, "color": "red"])

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.childByAppendingPath("size").setValue(99)
Java
Firebase ref = new Firebase(URL + "/widget");

// ALLOWED? Even though size is invalid, widget has children color and size,
// so write is allowed and the .write rule under color is ignored
Map<String,Object> map = new HashMap<String, Object>();
map.put("size", 99999);
map.put("color", "red");
ref.setValue(map);

// ALLOWED? Works even if widget does not exist, allowing us to create a widget
// which is invalid and does not have a valid color.
// (allowed by the write rule under "color")
ref.child("size").setValue(99);
REST
# ALLOWED? Even though size is invalid, widget has children color and size,
# so write is allowed and the .write rule under color is ignored
curl -X PUT -d '{size: 99999, color: "red"}' \
https://docs-examples.firebaseio.com/rest/securing-data/example.json

# ALLOWED? Works even if widget does not exist, allowing us to create a widget
# which is invalid and does not have a valid color.
# (allowed by the write rule under "color")
curl -X PUT -d '99' \
https://docs-examples.firebaseio.com/rest/securing-data/example/size.json

Esto ilustra las diferencias entre reglas .write y .validate. Como se demostró, todas estas reglas se deberían escribir con .validate, con la excepción posible de la regla newData.hasChildren(), que depende de si se deberían permitir las eliminaciones.

Cómo usar variables $ para captar segmentos de ruta de acceso

Para captar porciones de la ruta de acceso para una lectura o una escritura, puedes declarar variables de captura con el prefijo $. Este sirve de comodín y almacena el valor de esa clave para su uso dentro de las declaraciones de reglas:

{
  "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')"
        }
      }
    }
  }
}

Las variables dinámicas $ también se pueden usar en paralelo con nombres de rutas de acceso constantes. En este ejemplo, usamos la variable $other para declarar una regla .validate que garantiza que widget no tiene elementos secundarios aparte de title y color. Cualquier escritura que cree elementos secundarios adicionales fallaría.

{
  "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 }
    }
  }
}

Ejemplo de chat anónimo

Juntemos las reglas y creemos una aplicación de chat anónima y segura. Aquí, crearemos una lista de reglas y, a continuación, se incluye una versión funcional del chat:

{
  "rules": {
    // default rules are false if not specified
    // setting these to true would make ALL CHILD PATHS readable/writable
    // ".read": false,
    // ".write": false,

    "room_names": {
      // the room names can be enumerated and read
      // they cannot be modified since no write rule
      // explicitly allows this
      ".read": true,

      "$room_id": {
        // this is just for documenting the structure of rooms, since
        // they are read-only and no write rule allows this to be set
        ".validate": "newData.isString()"
      }
    },

    "messages": {
      "$room_id": {
        // the list of messages in a room can be enumerated and each
        // message could also be read individually, the list of messages
        // for a room cannot be written to in bulk
        ".read": true,

        // room we want to write a message to must be valid
        ".validate": "root.child('room_names/'+$room_id).exists()",

        "$message_id": {
          // a new message can be created if it does not exist, but it
          // cannot be modified or deleted
          ".write": "!data.exists() && newData.exists()",
          // the room attribute must be a valid key in room_names/ (the room must exist)
          // the object to write must have a name, message, and timestamp
          ".validate": "newData.hasChildren(['name', 'message', 'timestamp'])",

          // the name must be a string, longer than 0 chars, and less than 20 and cannot contain "admin"
          "name": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 20 && !newData.val().contains('admin')" },

          // the message must be longer than 0 chars and less than 50
          "message": { ".validate": "newData.isString() && newData.val().length > 0 && newData.val().length < 50" },

          // messages cannot be added in the past or the future
          // clients should use firebase.database.ServerValue.TIMESTAMP
          // to ensure accurate timestamps
          "timestamp": { ".validate": "newData.val() <= now" },

          // no other fields can be included in a message
          "$other": { ".validate": false }
        }
      }
    }
  }
}

Próximos pasos

Enviar comentarios sobre…

¿Necesitas ayuda? Visita nuestra página de asistencia.