# Seu frontend fala direto com o banco de dados. Veja como proteger o backend

Seu JavaScript expõe App ID e chave de cliente por design; o que eles podem fazer depende do backend. Medimos uma classe aberta (tudo), fechamos com quatro travas e medimos de novo, incluindo os 22 minutos em que a trava ficou meio aberta.

URL: https://www.back4app.com/blog/pt/como-proteger-o-backend-que-seu-frontend-acessa
Publicado: 2026-10-11 · Testado: 2026-09-17
Testado em: Backend: Back4app, plano Free, USA East · Sondas: bash + curl no macOS · Regras: Cloud Code com deploy pelo editor da dashboard
Publisher: Back4app Engineering

Abra seu app no navegador, abra as ferramentas de desenvolvedor e olhe a aba de rede. Dois headers saem com cada requisição ao seu backend: um App ID e uma chave. Qualquer pessoa lendo sua página tem os dois. Isso não é um vazamento. É assim que um frontend que fala com um backend gerenciado deve funcionar.

O que esses dois headers podem *fazer* é a questão inteira. Numa classe com permissões padrão, a resposta é: tudo. Listar todas as linhas, alterar qualquer linha, apagar todas. Medimos isso, doze requisições de quatro identidades, e depois fechamos a classe com quatro travas e medimos de novo após cada uma.

As quatro travas são permissões por classe, uma ACL por linha carimbada por um hook no backend, uma role verificada dentro de uma Cloud Function e uma master key (chave mestra) que nunca sai do servidor. Tudo abaixo foi medido em 17 de setembro de 2026, num backend Back4app, incluindo os 22 minutos em que a trava 1 ficou só parcialmente fechada.

**Pegue o código funcionando:** o repositório companion está em [github.com/templates-back4app/lockdown-lab](https://github.com/templates-back4app/lockdown-lab) — `probe.sh`, `seed.sh`, `lockdown.sh`, `cloud/main.js` e `migrate-acl.sh`. Rode a sonda antes e depois de cada trava e você obtém as tabelas deste post para o seu próprio backend.

## O que qualquer pessoa com a sua JavaScript key pode fazer agora?

Criamos uma classe `Note` do jeito que a maioria dos apps faz: a primeira escrita do cliente a criou. Depois semeamos dois usuários, `ana` e `bob`, e uma role `moderator` contendo `bob`, e rodamos doze requisições de quatro identidades. Anônimo significa só o App ID e a JavaScript key. A master key é o que um servidor teria.

![Arquitetura: quatro identidades chegam à classe Note através de quatro travas em ordem: permissões por classe, um hook beforeSave que carimba uma ACL só do dono, uma verificação de role dentro de uma Cloud Function e uma master key que nunca sai do backend](/blog/blog-assets/lock-down-a-backend-your-frontend-talks-to/architecture-diagram.svg)

Este é o estado 0, a classe como nasceu, às 15:08 UTC:

| Identidade → requisição | HTTP / código |
|---|---|
| anônimo (JS key) → listar notas | 200 |
| anônimo (JS key) → criar nota | 201 |
| anônimo (JS key) → apagar a nota da ana | 200 |
| ana (session token) → listar, ler, atualizar, criar | 200 / 200 / 200 / 201 |
| bob (session token) → ler a nota da ana | 200 |
| bob (session token) → atualizar a nota da ana | 200 |
| bob (session token) → apagar a nota da ana | 200 |
| master key → ler, apagar a nota da ana | 200 / 200 |

Toda linha verde. A terceira é a que merece atenção: alguém que nunca fez login, segurando apenas o que a sua página entrega, apagou os dados de outro usuário e recebeu um 200 por isso. Nada estava mal configurado. Esse é o padrão.

Como medimos: `probe.sh`, doze requisições `curl` de quatro identidades contra linhas novas, as destrutivas por último, rodado pelo menos uma vez por estado (três vezes para a classe aberta) em 17 de setembro de 2026.

A tabela de propagação vem de um amostrador de dez requisições anônimas de listagem a cada 30 segundos; o servidor que respondeu é o header `x-backend-server`.

## Por que a chave no seu frontend não é o problema?

Porque ela nunca foi um segredo. A JavaScript key, a Client key e a REST key são chaves de *cliente*: dizem qual app está chamando, para que o backend encontre seus dados e aplique suas regras. Elas foram feitas para estar num APK, numa página, num comando `curl`. Rotacioná-las não compra nada se as regras estão abertas.

A master key é o oposto. Ela ignora toda permissão deste post. Seu lugar é no Cloud Code, nas variáveis de ambiente de um servidor ou num script na sua máquina, e em nenhum cliente de nenhum tipo. O `.env` do repositório a guarda para as sondas e está no git-ignore por esse motivo.

Então o trabalho não é esconder a chave. O trabalho é dizer ao backend, por classe e por linha, o que cada identidade pode fazer. Isso acontece em três lugares: nas permissões da classe, na ACL de cada linha e no código que roda no backend.

## Como as permissões por classe barram requisições anônimas?

As permissões por classe (CLP) são a porta da classe. Para cada operação (find, get, create, update, delete, add field) elas dizem quem pode bater: todo mundo, só usuários autenticados, usuários e roles específicos, ou ninguém. A trava 1 é *Authenticated* (só usuários autenticados) em toda operação, o que deixa as linhas anônimas vermelhas e não mexe em mais nada.

A dashboard configura isso em *Database* → menu da classe → *Security* → *Class Level Permission*: desmarque *Public*, marque *Authenticated* em Read e Write. Fizemos a mesma coisa pelo terminal, pela schema API, porque um script é repetível e uma captura de tela não é:

```bash
# Stack: bash + curl (schema API, master key) | File: lockdown.sh
CLP='{"find":{"requiresAuthentication":true},"get":{"requiresAuthentication":true},"create":{"requiresAuthentication":true},"update":{"requiresAuthentication":true},"delete":{"requiresAuthentication":true},"addField":{}}'
curl -s -H "X-Parse-Application-Id: $APP_ID" -H "X-Parse-Master-Key: $MASTER_KEY" -H "Content-Type: application/json" \
  -X PUT "https://parseapi.back4app.com/schemas/Note" -d "{\"classLevelPermissions\":$CLP}"
```

![O diálogo Edit Class Level Permissions da classe Note na dashboard: Public desmarcado em Read, Write e Add field, Authenticated marcado em Read e Write, e um campo Add Role](/blog/blog-assets/lock-down-a-backend-your-frontend-talks-to/clp-dialog-authenticated.jpg)

Com a CLP no lugar, um create anônimo foi recusado na hora: `404`, código `101`, *Permission denied, user needs to be authenticated.* Um `count` anônimo recebeu um par diferente, `400` / `119`. Dois códigos para "não permitido", dependendo da operação; trate os dois.

## Quanto tempo uma mudança de permissão leva para chegar a todos os servidores?

Mais do que uma requisição, e essa foi a surpresa do dia. Mudamos a CLP às 15:08:40 UTC e sondamos às 15:10: o create anônimo foi recusado, a listagem anônima ainda era respondida. Vinte requisições de listagem seguidas deram 14 × 200 e 6 × 404, da mesma chave, no mesmo segundo. `GET /schemas/Note` mostrou as novas permissões o tempo todo. O caminho da requisição, não.

Deixamos um amostrador rodando: dez requisições anônimas de listagem a cada 30 segundos. Entre 15:14 e 15:30 o número de permitidas oscilou entre 10 de 10 e 0 de 10 e voltou.

O header de resposta `x-backend-server` explicou: todo 200 veio de um servidor, `shared752`, enquanto as negativas não traziam esse header. O app roda em várias instâncias, e uma delas manteve a regra antiga.

*Restart App* na página *Overview* às 15:25 informou *restarted successfully* e não mudou nada: 0 de 10 às 15:28, depois 8 de 10 de novo. O que encerrou a história foram dois deploys de Cloud Code por volta das 15:30, de um arquivo que não tinha mudado. A amostra seguinte leu 2 de 10, e toda amostra depois de 15:31:26 leu 0 de 10, pelo resto da tarde.

| Hora (UTC) | Requisições anônimas de listagem permitidas, de 10 |
|---|---|
| 15:14 – 15:16 | 10, 10, 10, 10 |
| 15:16 – 15:30 | 6, 9, 8, 8, 7, 6, 10, 10, 10, 10, 7, 6, 8, 7, 10, 8, 9, 6, 5, 4, 0, 8, 8, 7, 9 |
| 15:30:53 (depois dos deploys) | 2 |
| A partir de 15:31 | 0, 0, 0, … (24 amostras, até pararmos às 15:44) |

## Como um hook no backend dá a cada linha uma ACL só do dono?

A trava 1 mantém estranhos do lado de fora. Ela não faz nada quanto ao `bob`, que está logado e ainda pode ler, editar e apagar a nota da `ana`. Essa é a trava 2, a ACL: uma lista por linha de quem pode ler e quem pode escrever. O erro clássico é deixar o cliente defini-la; o cliente pode esquecer, e um comando `curl` vai esquecer. Então o backend a define, antes de a linha existir:

```javascript
// Stack: Node.js 22.x | Parse Server 8.x | File: cloud/main.js
// Lock 2 of 4: every Note gets an ACL that only its owner can read or write. The client cannot forget to set it,
// because the client never sets it — the backend does, before the row exists.
Parse.Cloud.beforeSave("Note", (request) => {
  const note = request.object;
  if (!note.isNew()) return;                                   // ACL is decided once, at creation
  const owner = request.user ?? note.get("owner");             // request.user = whoever holds the session token
  if (!owner) throw new Parse.Error(Parse.Error.SESSION_MISSING, "log in to create a note.");
  const acl = new Parse.ACL(owner);                            // owner: read + write; everyone else: nothing
  note.setACL(acl);
  note.set("owner", owner);
});
```

`request.user` é quem enviou o session token, resolvido pelo backend; o cliente não consegue alegar ser outra pessoa. `new Parse.ACL(owner)` concede leitura e escrita a esse usuário e nada a mais ninguém, incluindo o público.

O pointer `owner` é definido no mesmo gesto, para que uma query possa filtrar por ele e a função de moderador possa mostrar quem escreveu o quê.

![O editor de Cloud Code com cloud/main.js aberto no backend lockdown-lab: o hook beforeSave de Note que carimba a ACL do dono, e a Cloud Function moderatorList logo abaixo](/blog/blog-assets/lock-down-a-backend-your-frontend-talks-to/cloud-code-acl-hook.jpg)

Depois do deploy, toda nota que a `ana` cria carrega `ACL: { "<id da ana>": { read: true, write: true } }`, que o *Database Browser* mostra como o object id dela na coluna ACL. As linhas não vieram de um formulário que escrevemos; foram escritas pelas sondas, e o hook fez o resto.

![O Database Browser mostrando a classe Note com catorze objetos: a coluna ACL de toda linha traz o object id da ana, EBkE7EJ1sb, e a coluna owner aponta para o mesmo usuário](/blog/blog-assets/lock-down-a-backend-your-frontend-talks-to/database-note-after.jpg)

## O que o bob vê quando pede a nota da ana?

Nada, e não uma recusa. Com a ACL no lugar, `bob` lendo a nota da `ana` pelo id recebeu `404` / `101` *Object not found*. O mesmo status e código que um id inventado recebe. Atualizar e apagar: a mesma coisa. A ACL não diz *você não pode*; ela torna a linha invisível, e uma requisição de listagem simplesmente a omite.

Esse é o comportamento que você quer de um recurso de segurança e o comportamento que confunde um desenvolvedor às 2 da manhã. Se seus logs mostram 101 numa linha que você enxerga na dashboard, verifique a ACL antes de verificar o id.

| Identidade → requisição | Classe aberta | CLP + ACL + role |
|---|---|---|
| anônimo → listar, criar, apagar | 200 / 201 / 200 | 404 / 101 nas três |
| ana → ler, atualizar a própria nota | 200 / 200 | 200 / 200 |
| ana → listar, criar | 200 / 201 | 200 / 201 |
| bob → ler, atualizar, apagar a nota da ana | 200 / 200 / 200 | 404 / 101 nas três |
| master key → ler, apagar a nota da ana | 200 / 200 | 200 / 200 |

Uma nota sobre a única coisa que vimos e não conseguimos reproduzir. Na primeira rodada da sonda, segundos depois de `seed.sh` criar os usuários, as escritas da `ana` devolveram `209 Invalid session token` enquanto as leituras devolviam 200. Um minuto depois, três rodadas consecutivas não tiveram nenhum 209. Trate um 209 logo após o cadastro como motivo para rodar de novo, não como regra.

## Como uma role deixa moderadores lerem tudo sem abrir a classe?

Trava 3. Um `moderator` precisa ver todas as notas, e o jeito errado de dar isso é afrouxar a CLP ou adicionar a role à ACL de cada linha. O jeito certo é uma Cloud Function que verifica a role, faz a leitura no backend com a master key e devolve exatamente o que um moderador deve ver:

```javascript
// Stack: Node.js 22.x | Parse Server 8.x | File: cloud/main.js (continued)
// Lock 3 of 4: a role. Moderators can read everything through this function; nobody can through the class.
Parse.Cloud.define("moderatorList", async (request) => {
  if (!request.user) throw new Parse.Error(Parse.Error.INVALID_SESSION_TOKEN, "log in first.");
  const isMod = await new Parse.Query(Parse.Role).equalTo("name", "moderator").equalTo("users", request.user).first({ useMasterKey: true });
  if (!isMod) throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, "moderators only.");
  const notes = await new Parse.Query("Note").find({ useMasterKey: true });   // lock 4: the master key stays here
  return notes.map((n) => ({ id: n.id, text: n.get("text"), owner: n.get("owner")?.id }));
});
```

Medido: `ana` chamando `moderatorList` recebeu `119 moderators only.`; `bob`, que está na role, recebeu a lista completa, donos incluídos. A classe em si continuou tão travada quanto antes.

A role foi criada por `seed.sh` com a master key, porque criar uma role é por si só uma escrita privilegiada; a classe `_Role` na dashboard mostra uma linha, `moderator`, com `bob` na relation `users`.

O `useMasterKey: true` nas duas queries é a trava 4 em uma linha: a master key é usada, dentro do backend, por código cujo deploy você fez, numa query que você escolheu. Nenhum cliente jamais a tem, e um moderador que sai da role perde a lista na próxima chamada.

## Quais permissões uma classe nova recebe por padrão?

Todas, para todo mundo. Provamos isso uma segunda vez, de propósito: criamos uma classe `Draft` do jeito preguiçoso, escrevendo nela a partir de uma requisição anônima, e abrimos o *Database Browser*. Três linhas, e toda ACL diz **Public Read + Write**. A CLP era igual: tudo público, `addField` incluído.

![O Database Browser mostrando uma classe Draft com três objetos, cada um com a coluna ACL lendo Public Read + Write, criados por requisições anônimas](/blog/blog-assets/lock-down-a-backend-your-frontend-talks-to/database-draft-open.jpg)

Nada na dashboard avisa você. A classe simplesmente aparece, aberta, ao lado da que você travou uma hora atrás. Se a classe existe para que clientes criem linhas, a ordem é: crie a classe e a CLP primeiro (o *Add Class* da dashboard ou a schema API), faça o deploy do hook e só então deixe a primeira escrita do cliente acontecer.

## O que acontece com as linhas criadas antes da trava?

Elas ficam com a ACL com que nasceram. Um hook `beforeSave` roda nos saves feitos depois do deploy, e as catorze notas que nossas sondas criaram enquanto a classe estava aberta ainda diziam *Public Read + Write*. Qualquer um com a chave ainda conseguia ler essas, com trava ou sem. O `migrate-acl.sh` do repositório percorre a classe uma vez com a master key:

```bash
# Stack: bash + curl (master key) | File: migrate-acl.sh (core loop)
# rows with an owner → owner-only ACL; ownerless rows (anonymous drive-bys) → deleted
if [ "$action" = fix ]; then
  curl -s -o /dev/null "${MK[@]}" -X PUT "$BASE/classes/Note/$id" -d "{\"ACL\":{\"$owner\":{\"read\":true,\"write\":true}}}"
else
  curl -s -o /dev/null "${MK[@]}" -X DELETE "$BASE/classes/Note/$id"
fi
```

Ele corrigiu toda linha com dono, apagou as sem dono (as notas *de passagem* que as sondas anônimas tinham criado) e reportou 14 linhas restantes, todas com dono. Rode uma vez, depois que o hook estiver no ar e provado, e mantenha no repositório: a próxima classe que você travar vai precisar dele também.

## Como fica a tabela de doze requisições depois das quatro travas?

Este é o estado 2, medido às 15:33 UTC, depois que a CLP assentou e o hook e a role estavam no ar:

```text
identity → request                              HTTP / Parse code
anonymous (JS key)  → list notes           404 / 101
anonymous (JS key)  → create note          404 / 101
ana (session token) → list notes           200
ana (session token) → read her note        200
ana (session token) → update her note      200
ana (session token) → create note          201
bob (session token) → read ana's note      404 / 101
bob (session token) → update ana's note    404 / 101
master key          → read ana's note      200
anonymous (JS key)  → delete ana's note    404 / 101
bob (session token) → delete ana's note    404 / 101
master key          → delete ana's note    200
```

Mesma chave na página, mesmas doze requisições. Seis delas agora falham, e cada uma das seis é uma requisição que seu app nunca deveria ter conseguido fazer. As quatro que passam para a `ana` são as linhas dela. As duas que passam para a master key rodam de uma máquina que você controla.

`probe.sh` imprime essa tabela em uma rodada. Mantenha-a ao lado da classe. Quando um recurso novo precisar de uma permissão nova, o diff entre duas rodadas é a revisão.

## Quando um backend com chaves de cliente é o design errado?

Quando uma identidade nunca pode ler uma linha nem por acidente, e a regra é complexa demais para caber numa CLP mais uma ACL: dados multi-tenant com joins entre tenants, ou linhas cuja visibilidade depende de um cálculo.

Nesse caso o cliente não deveria consultar a classe de jeito nenhum. Deveria chamar uma Cloud Function que roda a query com a master key e devolve um resultado moldado, que é o que `moderatorList` já faz para um caso.

O outro caso é um segredo que o backend precisa guardar em nome do cliente: a chave de um provedor de pagamento, um token de terceiros. Esses vão no ambiente do Cloud Code ou num container, nunca numa classe que o cliente alcança, por mais travada que esteja.

Para uma classe de linhas que pertencem a usuários, as quatro travas bastam, e o conjunto todo é um `PUT` no schema, 20 linhas de Cloud Code e uma migração.

## O que você acrescentaria ao lockdown lab em seguida?

- **Uma nota compartilhada.** Adicione uma relation `sharedWith` e estenda o hook para conceder leitura a esses usuários. A sonda ganha uma linha: *carol lê uma nota compartilhada com ela → 200*.
- **Uma CLP por role.** Dê à role `moderator` escrita na classe pelo campo *Add Role* da CLP e veja quais linhas do bob ficam verdes. Depois decida se você prefere isso à função.
- **A mesma sonda em `_User`.** Por padrão, um usuário logado consegue ler as linhas de outros usuários. Trave `_User` com *Enforce private users* em Advanced Options (pago) ou com uma CLP, e sonde.
- **Uma verificação no deploy.** Rode `probe.sh` no CI contra um backend de staging e falhe o build se alguma linha que deveria estar vermelha ficar verde.
- **Usuários primeiro.** Se o app ainda não tem contas, esse é o post anterior: [Como adicionar autenticação a um app web ou Flutter sem escrever um servidor de auth](https://www.back4app.com/blog/add-signup-login-password-reset-without-an-auth-server).
