Teste o Cloud Firestore: conheça o banco de dados escalonável e flexível do Firebase e do Google Cloud Platform. Saiba mais sobre o Cloud Firestore.

Proteger seus dados

As regras do Firebase Database são uma configuração declarativa para seu 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.

Como estruturar suas regras

As regras do Firebase Database são feitas de expressões do tipo JavaScript contidas em um documento JSON. A estrutura das regras deve seguir a estrutura dos dados que você armazenou no banco de dados. 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

Existem 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 o que é um valor formatado corretamente, se ele tem atributos filhos e o tipo de dados.

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 timestamps criados com o firebase.database.ServerValue.TIMESTAMP do SDK.
root Um RuleDataSnapshot que representa o caminho raiz no database 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. De modo contrário, a variável newData contém os novos dados que existirão caso a operação de gravação tenha êxito. newData representa o resultado mesclado dos novos dados que serão gravados e dos dados existentes.

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 como ele existiria antes ou depois de um evento de gravação.

Considere este exemplo, que permite operações de gravação contanto que o valor do nó /allow_writes/ seja true, o nó pai não tenha um sinalizador readOnly 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 de .read e .write operam de cima a 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 o /bar/ seja lido pelo /foo/ sempre que ele tiver um baz filho com o valor true. A regra ".read": false em /foo/bar/ não tem nenhum efeito aqui, pois 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. Considere 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 existe nenhuma regra de leitura que conceda acesso a todos os dados em /records/, isso retornará um erro PERMISSION_DENIED. Se avaliarmos essa regra no simulador de segurança do Console do Firebase, podemos ver 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 permitia o acesso ao caminho /records/, mas observe que a regra de rec1 nunca foi avaliada porque não estava no caminho solicitado. Para buscar rec1, precisaríamos 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 seguinte regra baseada em consulta usa regras de segurança baseadas em usuários e regras baseadas em consultas para restringir o acesso a dados na coleção baskets somente aos carrinhos de compra 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 e mostrariam 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 seguintes expressões query. estão disponíveis nas regras do Firebase Database.

Expressões de regra baseada em consulta
Expressão Tipo Descrição
query.orderByKey
query.orderByPriority
query.orderByValue
booleano Verdadeiro para consultas ordenadas por chave, prioridade ou valor. Falso, em outros casos.
query.orderByChild string
nulo
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
número
booleano
nulo
Recupera os limites da consulta em execução ou, se não houver limite definido, retorna nulo.
query.limitToFirst
query.limitToLast
número
nulo
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 é aplicada 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

Vejamos a mesma estrutura, mas usando regras de .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 ilustra as diferenças entre regras de .write e .validate. Conforme demonstrado, todas essas regras devem ser gravadas usando .validate, com a possível exceção da regra newData.hasChildren(), que dependeria da permissão para exclusões.

Usar variáveis $ para capturar segmentos de caminhos

Você pode 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, estamos usando a variável $other para declarar uma regra .validate que garante que widget não tenha nenhum outro filho 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

Enviar comentários sobre…

Firebase Realtime Database
Precisa de ajuda? Acesse nossa página de suporte.