Início › Desafios › Desafio 02
Filtro dinâmico sem abrir a porta para injection
O desafio anterior era sobre volume. Este é sobre confiança: o momento em que o seu código passa a receber texto que veio de fora e usar esse texto para montar uma consulta. É aqui que mora a vulnerabilidade mais clássica de Apex, e também um erro mais silencioso que ela: a consulta que funciona, mas devolve para o usuário um campo que ele não deveria enxergar.
O cenário
A equipe de atendimento pediu uma busca flexível de contas: em vez de uma tela com campos fixos, a pessoa escolhe num menu qual campo quer filtrar e digita o valor. Hoje pode ser por nome, amanhã por CNPJ, depois por um campo personalizado que ainda não existe.
Como o campo é escolhido em tempo de execução, não dá para escrever a consulta pronta: ela precisa ser montada em texto. A primeira versão entregue foi mais ou menos assim, e é exatamente o que você não vai fazer:
// NAO faca isso: o valor digitado entra direto na consulta String soql = 'SELECT Id, Name FROM Account WHERE ' + campo + ' = \'' + valor + '\''; return Database.query(soql);
Se alguém digitar no campo de busca algo como x' OR Name != null OR Name = 'y, o filtro
deixa de filtrar e a consulta devolve a base inteira. E se o menu for manipulado para pedir um campo
sensível, a tela passa a exibir dado que aquele usuário não tem permissão de ler.
O que você deve construir
Crie uma classe Apex chamada BuscaSegura com um método público e
estático, exatamente com esta assinatura:
public static List<Account> buscarPorCampo(String campoApi, String valor)
O método deve receber o nome de API de um campo de Account e um valor, e devolver as
contas em que aquele campo é igual àquele valor.
Regras de aceite
- O valor nunca entra na string da consulta. Ele precisa chegar ao SOQL por bind.
- O campo é validado contra os campos reais do objeto, lidos do schema em tempo de execução. Não vale manter uma lista de campos permitidos escrita na mão: ela envelhece no dia em que alguém criar um campo novo.
- Campo que o usuário não pode ler não é consultado. A consulta precisa respeitar as permissões de quem chamou o método, não rodar com acesso total.
- Campo inexistente, campo inacessível, valor nulo ou entrada em branco devolvem lista vazia, sem lançar exceção.
- No máximo 50 registros por chamada.
O que este desafio avalia
- Se você sabe a diferença entre o que pode ser parametrizado por bind (valores) e o que não pode (nomes de campo, nomes de objeto).
- Se você usa o próprio schema como fonte de verdade em vez de uma lista fixa.
- Se você lembra que segurança de dados em Apex tem duas camadas: a injection e a permissão de leitura.
- Se o seu método falha de forma previsível, devolvendo vazio em vez de estourar erro na tela.
Travei. Me dá uma direção (sem entregar a resposta)
O desafio se resolve respondendo três perguntas na ordem:
- Como saber, em tempo de execução, se um campo existe no objeto? Pesquise por "Schema SObjectType fields getMap Apex". Atenção a um detalhe que pega muita gente: as chaves desse mapa vêm em minúsculas.
- Como saber se o usuário pode ler aquele campo? A partir do campo você chega no describe dele. Procure "DescribeFieldResult isAccessible". Vale checar também se o campo é filtrável, porque nem todo tipo de campo pode entrar num WHERE.
- Como passar o valor sem concatenar? Procure "Apex dynamic SOQL bind variable". Uma variável local em escopo pode ser referenciada com dois-pontos dentro da string da consulta.
Para a camada de permissão da consulta inteira, procure por "WITH USER_MODE SOQL". É uma cláusula que você acrescenta na própria consulta.
Como testar
Rode no Execute Anonymous. Os três primeiros casos são o uso normal; os três últimos são as tentativas de ataque e de erro que o seu método precisa absorver sem quebrar:
// 1. Caminho normal: campo padrao existente
System.debug('Por Name: ' + BuscaSegura.buscarPorCampo('Name', 'Acme'));
// 2. Case do nome do campo nao deve importar
System.debug('Por name minusculo: ' + BuscaSegura.buscarPorCampo('name', 'Acme'));
// 3. Outro campo padrao qualquer
System.debug('Por Industry: ' + BuscaSegura.buscarPorCampo('Industry', 'Technology'));
// 4. Tentativa de injection: precisa voltar vazio ou filtrar de verdade, nunca a base toda
List<Account> ataque = BuscaSegura.buscarPorCampo('Name', 'x\' OR Name != null OR Name = \'y');
System.debug('Tentativa de injection devolveu: ' + ataque.size() + ' registros');
System.assert(ataque.size() < 50 || ataque.isEmpty(), 'Injection nao pode devolver a base inteira');
// 5. Campo que nao existe
System.assertEquals(0, BuscaSegura.buscarPorCampo('CampoQueNaoExiste__c', 'x').size(), 'Campo inexistente deve voltar vazio');
// 6. Entradas degeneradas
System.assertEquals(0, BuscaSegura.buscarPorCampo(null, 'x').size(), 'Campo nulo deve voltar vazio');
System.assertEquals(0, BuscaSegura.buscarPorCampo('Name', null).size(), 'Valor nulo deve voltar vazio');
System.debug('Todos os casos passaram');
O ponto que fica: toda vez que texto de fora entra no seu código, ele precisa passar por uma porta. Valor passa pela porta do bind. Nome de campo e de objeto passam pela porta do describe. O que não passa por porta nenhuma é o que vira incidente.