Saltar para o conteúdo

Segurança Appwrite vs Supabase: os mesmos ataques nos dois

Ver como MarkdownAbrir em ClaudeAbrir em ChatGPTAbrir em Perplexity

A pergunta também é copiada. Se o chat abrir vazio, cole-a.

Em resumo

Ambos bloqueiam uma tabela nova por omissão: o Appwrite respondeu 401 a um convidado e a um utilizador autenticado, e o Supabase responde 401/403 até dar a tabela aos roles da API. A diferença aparece quando uma regra está errada ou um pedido passa de um utilizador para outro. No Supabase, um update na linha de outro utilizador devolveu 200 [] e uma policy de insert demasiado larga deixou um utilizador juntar-se a outra empresa; no Appwrite, todas as tentativas entre utilizadores na minha execução acabaram em 401 ou 404, e um utilizador não podia dar a sua linha a ninguém senão a si próprio ou a todos. As chaves de servidor do Appwrite têm scopes; uma secret key do Supabase ignora todas as policies.

Sou o Denys, engenheiro full-stack e de IA. A maioria das apps que construo ou corrijo corre em Supabase, e os clientes que ainda estão a escolher um backend perguntam-me como se compara o Appwrite em segurança. Uma lista de funcionalidades não responde a isso, por isso enviei aos dois os mesmos pedidos.

Como testei. Appwrite 2.3.0, self-hosted com Docker a 30 de outubro de 2026, pela sua API REST como convidado, como dois utilizadores autenticados A e B e com duas chaves de servidor. O lado Supabase é a minha execução numa stack local (CLI 2.109.1, Postgres 17.6) de 24 de outubro de 2026 com os mesmos roles, descrita por inteiro em Exemplos de RLS policy no Supabase. Linhas inventadas nos dois.

Como protegem os dados o Appwrite e o Supabase?

O Supabase protege as tabelas com o Postgres: um grant decide se os roles da API chegam sequer a uma tabela, e as policies de row level security, escritas em SQL, decidem que linhas cada pedido vê e escreve. O Appwrite guarda as permissões junto dos dados: uma tabela tem uma lista de roles que podem ler, criar, atualizar ou apagar, e com a row security ligada cada linha traz a sua própria lista. Na documentação de permissões do Appwrite o acesso só é concedido, nunca negado, e o utilizador precisa dele ao nível da tabela ou da linha, não dos dois.

Portanto, a pergunta não é qual dos dois tem controlo de acessos. Ambos têm. É o que cada um lhe deixa errar.

O que faz cada um com uma tabela nova?

Pedido Appwrite, sem permissões definidas Supabase, RLS ligado, sem grant (24 out)
Convidado lista linhas 401 user_unauthorized 401 permission denied
Utilizador autenticado lista linhas 401 user_unauthorized 403 permission denied
Utilizador autenticado adiciona linha 401 No permissions provided for action ‘create’ não testado

Uma tabela recém-criada está fechada nos dois. No Supabase isso é novidade: as tabelas criadas depois de 30 de outubro de 2026 deixam de receber grants da Data API automaticamente, o que expliquei em Supabase, 30 de outubro: o que falha com os novos grants.

O que acontece quando uma tabela está aberta a todos?

As duas definições «abertas» não são a mesma coisa. No Appwrite dei à tabela uma só permissão, read("any"). No Supabase, o estado aberto mais comum é o RLS desligado com os grants por omissão ainda no sítio.

Pedido de um convidado Appwrite, read("any") Supabase, RLS desligado, grants por omissão (24 out)
Listar linhas 200, as duas linhas 200, as duas linhas
Apagar uma linha 401 200, as duas linhas apagadas

O «público» do Appwrite é só de leitura, a não ser que dê também escrita. Uma tabela Supabase com RLS desligado é legível e gravável por qualquer pessoa com a publishable key, e é por isso que «RLS disabled in public» é a primeira coisa que verifico.

Como funcionam as «linhas próprias» em cada um?

No Appwrite liguei a row security e dei à tabela uma só permissão, create("users"). Quando A criou uma linha sem indicar permissões, o Appwrite definiu-as por si: read, update e delete só para o utilizador A. No Supabase a mesma intenção são quatro policies sobre (select auth.uid()) = user_id.

Pedido Appwrite (30 out) Supabase (24 out)
Convidado lista 200, 0 linhas 401 (sem grant para anon)
A lista linhas 200, as 2 linhas de A 200, as 2 linhas de A
A lê a linha de B 404 row_not_found não devolvida na lista
A atualiza linha de B 401 200 []
A apaga linha de B 401 200 []
O mesmo pedido, dois backends. O utilizador A edita uma linha do utilizador B, pela API REST de cada um. Supabase, policy só do dono: Utilizador A → edita linha de B → 200 [] → 0 linhas, sem erro; Appwrite, row security ligada: Utilizador A → edita linha de B → 401 → erro devolvido.
Figura 1. Os meus testes: stack Supabase local (CLI 2.109.1), 24 de outubro de 2026; Appwrite 2.3.0 self-hosted, 30 de outubro de 2026.

Nenhum deixou vazar a linha de B. A diferença é a resposta. O Supabase filtra as linhas que uma policy esconde, por isso um update que não apanhou nada é um sucesso com zero linhas; uma app que trata «sem erro» como «guardado» diz ao utilizador que a edição funcionou. O Appwrite recusa com um erro que a app não pode deixar de ver.

Um utilizador pode passar os seus dados a outra pessoa?

É aqui que o código gerado mais falha, por isso testei nos dois.

Pedido de A Appwrite Supabase (24 out)
Criar linha legível pelo utilizador B 401, Permissions must be one of: any, users, user:A… depende da policy
Mudar a própria linha para B ser o dono 401, a mesma mensagem 403 com um with check completo; 200 com with check (true)
Criar linha legível por todos 201, e um convidado listou-a a seguir só se uma policy o permitir

O Appwrite limita o que um utilizador pode conceder aos roles que ele próprio tem: any, users, o seu utilizador e as suas equipas. Isso bloqueou o gesto «dar a minha linha a outra pessoa» sem qualquer regra minha. Não impediu A de publicar a sua própria linha para todos com read("any"). Se uma linha nunca deve ser pública, não deve ser o cliente a definir as suas permissões; crie-a a partir de uma função de servidor.

No Supabase o resultado é o que a policy disser. Um with check completo recusou a mudança com 403; with check (true), que alguns geradores escrevem, deixou A mover a linha para a conta de B.

Um utilizador pode juntar-se à empresa de outra pessoa?

No Supabase, o acesso por equipa costuma ser uma tabela memberships mais policies. Na minha execução, a policy de insert que muitos tutoriais sugerem, with check (user_id = auth.uid()), deixou A adicionar-se à empresa 2 com 201 Created e depois ler o seu projeto de salários.

O Appwrite tem equipas integradas. B criou uma equipa; A tentou adicionar-se como membro e como dono. Os dois pedidos devolveram 404 team_not_found: A nem sequer via a equipa. A pertença a uma equipa é decidida pelo servidor, não por uma regra que escreva, por isso esta fuga em concreto não existe. As linhas continuam a precisar de permissões read("team:…") para serem partilhadas, e essas define-as você.

O que abre uma chave de servidor?

Chave Appwrite Supabase (do meu teste anon vs service role)
Chave de servidor completa 200, as 4 linhas dos dois utilizadores as 3 linhas, RLS ignorado
Chave com scope estreito (users.read) 401, missing scope rows.read não há chaves com scope

Uma secret key do Supabase corresponde a service_role, que tem BYPASSRLS; o que abre está em anon key vs service role key. Uma chave do Appwrite também ignora as permissões de linha, mas só para os scopes com que foi criada, por isso uma chave para uma tarefa que gere utilizadores não consegue ler as suas tabelas. Qualquer das chaves num bundle de browser é uma fuga total de tudo o que ela alcança.

Duas constatações menores sobre o Appwrite: um convidado e um utilizador autenticado a pedir a lista de tabelas receberam ambos 401 (a chamada precisa de tables.read, que só as chaves têm), e read("users") significa qualquer conta autenticada, a mesma armadilha de auth.uid() is not null no Supabase quando o registo está aberto.

Qual é mais seguro?

Nenhum é seguro ou inseguro por omissão; ambos partiram fechados. Diferem no sítio onde um erro se pode esconder:

Pergunta Appwrite Supabase
Onde vivem as regras de acesso Listas de roles em tabelas e linhas Grants e policies em SQL
Uma escrita negada entre utilizadores Erro (401 / 404) 200 com zero linhas em update e delete
Dar linhas a outros Só aos roles que o utilizador tem O que a policy permitir
Regras sobre valores de colunas, nível MFA Não nas permissões; exige código de servidor Na policy, p. ex. aal2 para linhas sensíveis
Chaves de servidor Com scope A secret key ignora todas as policies

O modelo do Appwrite é mais estreito, e na minha execução o modelo estreito recusou todos os truques entre utilizadores sem eu escrever nada. O modelo do Supabase é SQL, por isso consegue exprimir regras que as permissões do Appwrite não conseguem, como «só com MFA» ou «só linhas das empresas a que pertenço», e também consegue exprimir a regra errada. Se a sua equipa não vai ler as policies, os valores por omissão do Appwrite perdoam mais. Se precisa de regras que dependam dos dados, o Supabase deixa-o escrevê-las, e depois têm de ser testadas; o método que uso está em como testar o Supabase RLS.

Como verificar um projeto Supabase?

A Auditoria de segurança Supabase gratuita lê o seu projeto só em leitura e assinala tabelas com RLS desligado, policies sempre verdadeiras, policies de escrita para convidados e grants que dão mais do que as policies deixam supor. O que o Supabase cobre por si e o que lhe deixa a si está em o Supabase é seguro?. Não existe equivalente para o Appwrite; no Appwrite, corra os pedidos acima como dois utilizadores. Para o Firebase, corri o mesmo tipo de ataques em exemplos de regras de segurança do Firebase.

Limites. Um Appwrite 2.3.0 self-hosted num portátil e uma stack Supabase local, com seis dias de intervalo, com linhas inventadas. O Appwrite Cloud pode diferir do self-hosted. Storage, funções, realtime, definições de auth e certificados de conformidade não são comparados aqui.

A escolher um backend para uma app com dados privados?

Terei gosto em ver o seu modelo de acessos consigo. Sem pitch.

Fale comigo

© 2026 Denys Zaichenko. Artigo original: https://madsalt.tech/pt/blog/appwrite-vs-supabase/