Proteger seus dados

As regras do Realtime Database são uma configuração declarativa do banco de dados. Isso significa que as regras são definidas separadamente da lógica do produto. Isso oferece diversas vantagens: os clientes não são responsáveis por aplicar a segurança, implementações inadequadas não comprometerão seus dados e, talvez o mais importante de tudo, não é preciso ter um referencial intermediário, como um servidor, para proteger os dados do mundo.

Estruturar suas regras

As regras do Realtime Database são feitas de expressões do tipo JavaScript contidas em um documento JSON. A estrutura das regras precisa seguir a estrutura dos dados que você armazenou no banco. Por exemplo, suponha que você esteja acompanhando uma lista de mensagens e tenha dados parecidos com os seguintes:

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

As regras devem ser estruturadas de maneira semelhante. Veja um exemplo de um conjunto de regras que pode fazer sentido para essa estrutura de dados.

{
  "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 regras de segurança

Há três tipos de regras para aplicar a segurança: .write, .read e .validate. Veja um breve resumo da finalidade delas:

Tipos de regra
.read Descreve se e quando os dados podem ser lidos pelos usuários.
.write Descreve se e quando os dados podem ser gravados.
.validate Define a formatação correta do valor, o tipo de dados e se o valor tem atributos filhos.

Variáveis predefinidas

Há muitas variáveis predefinidas úteis que podem ser acessadas dentro de uma definição de regra de segurança. Usaremos a maioria delas nos exemplos abaixo. Vejamos um breve resumo de cada uma e um link para a referência da API relacionada.

Variáveis predefinidas
now O horário atual em milissegundos desde a era Linux. Isso funciona muito bem para validar carimbos de data/hora criados com o firebase.database.ServerValue.timESTAMP do SDK.
root Um RuleDataSnapshot que representa o caminho raiz no banco de dados do Firebase como ele existia antes da tentativa de operação.
newData Um RuleDataSnapshot que representa os dados como eles existiriam após a tentativa de operação. Inclui os novos dados que estão sendo gravados e dados existentes.
data Um RuleDataSnapshot que representa os dados como eles existiam antes da tentativa de operação.
$ variables Um caminho curinga usado para representar códigos e chaves filhas dinâmicas.
auth Representa o payload do token de um usuário autenticado.

Essas variáveis podem ser usadas em qualquer lugar nas suas regras. Por exemplo, as regras de segurança abaixo garantem que os dados gravados no nó /foo/ sejam strings com 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"
    }
  }
}

Dados existentes x dados novos

A variável data predefinida é usada para consultar os dados antes de ocorrer uma operação de gravação. Por outro lado, a variável newData contém os novos dados que existirão se a operação de gravação for bem-sucedida. newData representa o resultado mesclado dos novos dados que estão sendo gravados e os dados atuais.

Por exemplo, esta regra permite criar novos registros ou excluir os existentes, mas não permite fazer mudanças nos dados não nulos existentes:

// 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()"

Referenciar dados em outros caminhos

Todos os dados podem ser usados como critério para regras. Usando as variáveis predefinidas root, data e newData, podemos acessar qualquer caminho da forma como ele existiria antes ou depois de um evento de gravação.

Considere este exemplo, que permite operações de gravação desde que o valor do nó /allow_writes/ seja true, o nó pai não tenha um sinalizador readOnly definido e haja um filho chamado foo nos dados recém-gravados:

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

Regras de leitura e gravação são aplicadas em cascata

As regras .read e .write funcionam de cima para baixo, com regras em níveis menos profundos que substituem regras de níveis mais profundos. Se uma regra conceder permissões de leitura ou gravação em um caminho específico, ela também concederá acesso a todos os nós filhos nesse caminho. Considere a seguinte estrutura:

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

Essa estrutura de segurança permite que /bar/ seja lido sempre que /foo/ contiver um baz filho com o valor true. A regra ".read": false em /foo/bar/ não tem efeito aqui porque o acesso não pode ser revogado por um caminho filho.

Mesmo que isso não pareça intuitivo de imediato, essa é uma parte eficiente da linguagem das regras e permite implementar privilégios de acesso muito complexos com esforço mínimo. Isso será demonstrado quando abordarmos a segurança baseada no usuário posteriormente neste guia.

Vale ressaltar que regras .validate não são aplicadas em cascata. Todas as regras de validação devem ser atendidas em todos os níveis da hierarquia para que uma gravação seja permitida.

Regras não são filtros

As regras são aplicadas de maneira atômica. Isso significa que uma operação de leitura ou gravação falhará imediatamente se não houver uma regra nesse local ou em um local pai que conceda acesso. Mesmo que cada caminho filho afetado seja acessível, a leitura no local pai falhará totalmente. Suponha esta estrutura:

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

Sem entender que as regras são avaliadas atomicamente, pode parecer que buscar o caminho /records/ retornaria rec1, mas não rec2. O resultado real, no entanto, é um erro:

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

Como a operação de leitura em /records/ é atômica, e não há regra de leitura que conceda acesso a todos os dados em /records/, isso gerará um erro PERMISSION_DENIED. Se avaliarmos essa regra no simulador de segurança do Console do Firebase, vemos que a operação de leitura foi negada:

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

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

A operação foi negada porque nenhuma regra de leitura permitiu acesso ao caminho /records/, mas a regra para rec1 nunca foi avaliada porque não estava no caminho solicitado. Para buscar rec1, precisamos acessá-lo diretamente:

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!

Regras baseadas em consultas

Embora não seja possível usar regras como filtros, você pode limitar o acesso a subconjuntos de dados usando parâmetros de consulta nas suas regras. Use as expressões query. nas suas regras para conceder acesso de leitura ou de gravação com base nos parâmetros de consulta.

Por exemplo, a regra baseada em consulta a seguir usa regras de segurança baseadas em usuário e regras baseadas em consulta para restringir o acesso aos dados na coleção baskets apenas aos cestos de compras do usuário ativo:

"baskets": {
  ".read": "auth.uid != null &&
            query.orderByChild == 'owner' &&
            query.equalTo == auth.uid" // restrict basket access to owner of basket
}

A seguinte consulta, que inclui os parâmetros de consulta na regra, seria bem-sucedida:

db.ref("baskets").orderByChild("owner")
                 .equalTo(auth.currentUser.uid)
                 .on("value", cb)                 // Would succeed

No entanto, as consultas que não incluem os parâmetros na regra falhariam com um erro PermissionDenied:

db.ref("baskets").on("value", cb)                 // Would fail with PermissionDenied

Você também pode usar regras baseadas em consulta para limitar a quantidade de dados que um cliente baixa por meio das operações de leitura.

Por exemplo, a seguinte regra limita o acesso de leitura somente aos primeiros 1.000 resultados de uma consulta, ordenados por prioridade:

messages: {
  ".read": "query.orderByKey &&
            query.limitToFirst <= 1000"
}

// Example queries:

db.ref("messages").on("value", cb)                // Would fail with PermissionDenied

db.ref("messages").limitToFirst(1000)
                  .on("value", cb)                // Would succeed (default order by key)

As expressões query. a seguir estão disponíveis nas regras do Realtime Database.

Expressões de regra baseada em consulta
Expression Tipo Descrição
query.orderByKey
query.orderByPriority
query.orderByValue
boolean Verdadeiro para consultas ordenadas por chave, prioridade ou valor. Falso, em outros casos.
query.orderByChild string
null
Usa uma string para representar o caminho relativo para um nó filho. Por exemplo: query.orderByChild == "address/zip" Se a consulta não for ordenada por um nó filho, esse valor é nulo.
query.startAt
query.endAt
query.equalTo
string
number
boolean
null
Recupera os limites da consulta em execução ou, se não houver limite definido, retorna nulo.
query.limitToFirst
query.limitToLast
number
null
Recupera o limite na consulta em execução ou, se não houver limite definido, retorna nulo.

Validação de dados

Deve-se aplicar estruturas de dados e validar o formato e o conteúdo dos dados usando as regras .validate, que são executadas somente após uma regra .write conceder acesso. Veja abaixo um exemplo de definição de regra .validate que permite datas somente no formato AAAA-MM-DD entre os anos de 1900 e 2099, o que é verificado usando uma expressão 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])$/)"

As regras .validate são o único tipo de regra de segurança que não são aplicadas em cascata. Se qualquer regra de validação falhar em algum registro filho, toda a operação de gravação será rejeitada. Além disso, as definições de validação são ignoradas quando os dados são excluídos, ou seja, quando o novo valor que está sendo gravado é null.

Talvez esses pontos possam parecer triviais. No entanto, são recursos importantes para definir regras importantes do Firebase Realtime Database. Considere as seguintes regras:

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

Com esta variante em mente, analise os resultados das operações de gravação a seguir:

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

Agora vamos analisar a mesma estrutura, mas usando regras .write em vez 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()"
      }
    }
  }
}

Nesta variante, todas as operações a seguir terão ê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

Isso representa as diferenças entre as regras .write e .validate. Como demonstrado, todas essas regras devem ser gravadas usando .validate, com a possível exceção da regra newData.hasChildren(), que dependerá da possibilidade de exclusões.

Usar variáveis $ para capturar segmentos de caminhos

É possível capturar partes do caminho para uma leitura ou gravação declarando variáveis de captura com o prefixo $. Ela serve como um curinga e armazena o valor dessa chave para uso em declarações de regras:

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

As variáveis $ dinâmicas também podem ser usadas paralelamente com nomes de caminhos de constantes. Neste exemplo, a variável $other está sendo usada para declarar uma regra .validate que garante que widget não tenha filhos além de title e color. Qualquer gravação que resulte na criação de um filho adicional falhará.

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

Exemplo de bate-papo anônimo

Vamos reunir as regras e criar um aplicativo de bate-papo anônimo seguro. Aqui, listaremos as regras e uma versão funcional do bate-papo foi incluída abaixo:

{
  "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óximas etapas