Quando o assunto é segurança em desenvolvimento, é comum associar o início da jornada à adoção de ferramentas como SAST e DAST, que analisam o código estaticamente e testam a aplicação em execução em busca de vulnerabilidades. Ou à criação de equipes especializadas, como red team e blue team, chamadas em alguma fase do SDLC (Software Development Life Cycle). Essas estratégias existem por bons motivos e um CTO precisa delas. Mas elas chegam tarde se o software nasce todos os dias com decisões e práticas frágeis. E quando a organização espera que um único time cuide do assunto, esse time vira gargalo, acumulando backlog de problemas que poderiam nem ter existido.
Segurança é atributo de qualidade.
Assim como performance, manutenibilidade, observabilidade e confiabilidade, segurança precisa permear o desenvolvimento desde o refinamento da história até a implementação, revisão, teste, deploy e operação. Quando uma funcionalidade é estimada sem considerar validação, autorização, exposição de dados, abuso de fluxo, rastreabilidade e comportamento em falha, o time está deixando qualidade para depois, com alto risco de prejuízo financeiro e de imagem (que também impacta em financeiro).
E qualidade deixada para depois costuma voltar como incidente, um juros alto a pagar.
Às vezes volta como bug, às vezes como retrabalho, às vezes como indisponibilidade e, nos casos mais graves, como notícia. Uma falha de segurança exposta publicamente causa um tipo de prejuízo difícil de medir, que é a perda de confiança. O impacto financeiro pode ser calculado, o custo jurídico pode ser estimado, o esforço técnico pode ser planejado. Mas o dano de imagem, a sensação de descuido e a quebra de confiança com clientes, parceiros e mercado são muito mais difíceis de calcular e reparar.
Por isso, ao refinar uma história, lembre-se de reservar tempo para desenvolver essa capacidade. Não trate segurança como um checklist externo, algo que alguém valida no fim, ou uma tarefa que pode ser delegada para uma IA, uma skill, um plugin ou um scanner. Essas ferramentas ajudam, mas não substituem consciência e maturidade técnica.
Use IA, use ferramenta, use automação. Independente disso, aprenda os conceitos por trás dessas decisões e valide se aquilo que está sendo entregue fecha as lacunas que você, como profissional, precisa garantir.
Quando falamos de segurança, é comum imaginar criptografia, autenticação, autorização, exploração sofisticada ou alguma brecha obscura. Muitas falhas, porém, surgem por motivos bem menos cinematográficos. Ausência de decisões pequenas, falta de consciência técnica e escolhas frágeis repetidas no dia a dia do desenvolvimento.
Vamos explorar algumas delas.
A tela já valida.
O botão já está escondido.
O usuário já tem token.
O usuário já tem JWT.
E parece que está tudo bem, mas infelizmente não está.
Boa parte das falhas de segurança em aplicações acontece quando o desenvolvedor confia a proteção da aplicação a algo que existe apenas para ajudar a experiência do usuário. Frontend melhora usabilidade, token identifica um usuário, claims carregam contexto. Nenhum desses elementos, por si só, deveria ser tratado como um sistema seguro.
1. Validação "já existe no frontend"
Esse é o erro mais bobo da lista e mais comum do que parece.
O frontend limita o tamanho do campo, aplica máscara, bloqueia valores inválidos, desabilita botão, valida formato de e-mail, restringe upload e impede combinações erradas na tela. Então se assume que o backend receberá sempre aquilo que a interface permite enviar.
Só que o backend não conversa apenas com uma tela. E mesmo que conversasse, isso não impediria um usuário de "temperar" os dados antes de enviar.
Ele pode usar Postman, curl, scripts, bots, integrações, usuários maliciosos, extensões de navegador e qualquer cliente capaz de montar uma requisição HTTP.
Tenha em mente que validação no frontend é experiência e validação no backend é segurança e consistência.
Quando o backend aceita qualquer coisa porque "a tela já trata", a aplicação deixa uma parte da sua segurança nas mãos do cliente. E o cliente é justamente a parte que você não controla.
A correção é simples. Toda regra que protege consistência, permissão, limite ou integridade precisa existir no backend. O frontend pode repetir essa validação para melhorar a experiência e, em última instância, evitar chamadas desnecessárias ao backend, mas nunca deve ser a única barreira.
Esse padrão de erro também tem nome. No CWE (Common Weakness Enumeration), catálogo que enumera fraquezas conhecidas de software, ele aparece como CWE-602 (Client-Side Enforcement of Server-Side Security), a fraqueza de confiar ao cliente a aplicação de uma regra de segurança que pertence ao servidor. E a mesma classificação cobre o próximo erro da lista.
2. Permissão "gerenciada" no frontend
A segunda falha é parecido e bem mais desastrosa.
A aplicação esconde o botão para quem não tem autorização, remove o menu para quem não tem perfil, omite campos sensíveis, bloqueia a tela de configuração, muda a navegação conforme a role do usuário.
Isso é bom para a interface. Então vamos repetir o mantra deste artigo:
Interface cuida da experiência.
A fronteira de segurança e consistência fica no backend.
Se a API continua aceitando as chamadas, esconder o botão só reduz a chance de um usuário comum clicar. Alguém curioso ainda pode chamar o endpoint diretamente.
A pergunta que importa deixa de ser "esse usuário vê essa ação na tela?" e passa a ser "o backend rejeita essa ação se ela for chamada mesmo assim?".
Essa permissão pode ser validada no API Gateway? Pode. Em alguns cenários, até deve. Isso mitiga custos e chamadas ao backend, centraliza políticas, reduz tráfego indevido e bloqueia chamadas óbvias, mas não elimina a responsabilidade do serviço. O serviço ainda precisa proteger a regra que executa.
Porque nem toda chamada passa necessariamente pelo caminho que você imaginou. Pode existir outro consumidor, outro fluxo interno, outro job, outro endpoint, outra integração, outro serviço ou simplesmente uma rota esquecida. Quando a segurança depende de uma única camada externa, qualquer desvio de arquitetura pode virar uma condição explorável.
Toda ação sensível precisa ser autorizada no serviço. Criar, editar, excluir, aprovar, cancelar, consultar, exportar, reprocessar, alterar permissão, gerar cobrança. Se a ação tem impacto, ela precisa passar por uma decisão de autorização, independentemente de quem chama.
Esse é um bom ponto para trazer uma lente de Zero Trust para o desenvolvimento.
Zero Trust, no contexto de software, vai além de rede, firewall, identidade corporativa ou buzzword de fornecedor. É uma postura de construção que evita presumir confiança porque a chamada veio de dentro, porque passou pelo gateway, porque existe token, porque o usuário viu uma tela, porque outro serviço chamou ou porque "ninguém deveria saber essa URL".
Cada camada deve validar o que é relevante para sua responsabilidade. A interface melhora a experiência, o gateway aplica políticas transversais, o serviço protege a regra de negócio e o banco pode reforçar isolamento quando fizer sentido. Segurança é uma propriedade da arquitetura, distribuída entre as camadas. Arquiteturas robustas implementam controles de segurança em profundidade, reduzindo a dependência de qualquer mecanismo isolado.
O frontend pode decidir o que mostrar.
O backend definitivamente deve decidir o que permitir.
Existe ainda uma abordagem interessante quando a arquitetura decide levar REST mais a sério, o HATEOAS (Hypermedia as the Engine of Application State). No modelo de maturidade de Richardson, que classifica APIs REST em níveis de aderência ao estilo, o nível mais maduro entrega junto com os dados os links e as ações possíveis para aquele recurso naquele contexto.
Em vez de o frontend "adivinhar" o que pode ser feito com base em role, estado ou regra duplicada, o backend devolve quais ações estão disponíveis. Um pedido pode vir com links para cancelar, pagar, reabrir ou acompanhar, dependendo do usuário, do estado do pedido e das regras do sistema. O frontend apresenta com base no que foi enviado.
Isso não elimina a validação no backend quando a ação for chamada, mas melhora o desenho da experiência. A interface deixa de se preocupar com permissão e tratamentos a partir de dados e estado e passa a refletir decisões tomadas mais perto das regras. O backend orienta o que pode ser exibido e continua responsável por negar qualquer tentativa inválida.
3. Autorização feita só por "ter token"
Esse ponto se conecta diretamente com o anterior. Se o frontend não pode ser a fronteira de segurança, o backend precisa saber diferenciar duas coisas que muita gente mistura: autenticação e autorização.
O usuário fez login, recebeu um token e a assinatura confere. Ótimo. O sistema sabe quem ele é, mas ainda falta validar o que esse usuário pode fazer.
Autenticação identifica.
Autorização define o que pode ver e fazer.
O raciocínio perigoso costuma ser: o usuário fez login, o token é válido, a assinatura confere, a sessão existe. Logo, a requisição pode passar.
Esse raciocínio abre uma brecha de segurança muito perigosa, porque alguém pode acessar ou alterar um recurso que pertence a outra pessoa, outra empresa, outro contexto de negócio ou outro tenant (cada cliente isolado dentro de um sistema compartilhado).
Um usuário autenticado pode acessar qualquer pedido?
Pode baixar qualquer nota fiscal?
Pode consultar qualquer cliente?
Pode reprocessar qualquer pagamento?
Pode transferir valor de qualquer conta?
Pode alterar qualquer configuração?
Pode acessar dados de qualquer loja, empresa, tenant ou conta?
Ter token responde apenas essa pergunta: quem é esse usuário?
Contudo isso não é suficiente. A próxima pergunta deve ser: o que esse usuário pode fazer neste recurso, neste contexto, agora?
Essa falha aparece em cenários simples como:
GET /orders/123GET /invoices/456POST /payments/789/refundPATCH /users/10/role
O sistema verifica se o token é válido, mas não verifica se aquele usuário pode acessar aquele recurso, aquele pedido, aquela nota, aquele pagamento ou aquele perfil.
Ao fazer GET /orders/123, a pergunta não pode parar em "o token é válido?". Ela precisa encadear com outras. Esse pedido pertence ao usuário logado? Esse usuário pode buscar essa informação? Esse pedido pertence ao tenant correto? O estado desse pedido permite essa ação? Existe alguma regra de negócio que limite esse acesso?
Resolver isso exige menos sofisticação do que parece. Você não precisa implementar um motor completo de ABAC (autorização baseada em atributos) nem replicar algo como o IAM da AWS. Na maioria dos casos, basta validar se o usuário é dono do recurso informado ou tem autorização explícita para vê-lo ou alterá-lo. Uma consulta que confere o vínculo entre usuário e recurso já fecha a maior parte dessa brecha.
É assim que nasce uma das falhas mais comuns em sistemas, o usuário autenticado com a possibilidade de acessar recurso que não deveria acessar. A OWASP dá nome a isso. No OWASP API Security Top 10, ela ocupa a primeira posição como Broken Object Level Authorization (BOLA), evolução do que por anos foi chamado de IDOR (Insecure Direct Object Reference). No OWASP Top 10 tradicional, integra a categoria Broken Access Control, que lidera a lista desde a edição de 2021.
E detalhe importante: segurança por obscuridade não é segurança.
Não adianta acreditar que ninguém vai trocar o ID na URL, descobrir o endpoint, repetir uma chamada, inspecionar o tráfego ou montar uma requisição manual. Se o backend aceita a operação apenas porque o token existe, o sistema confundiu identidade com permissão.
Autorização precisa considerar ação, recurso e contexto.
Não basta perguntar "está logado?".
É preciso perguntar "pode executar essa ação sobre esse recurso?".
4. Confiança cega em claims do JWT
Encadeando com isso, a próxima falha é observada quando o time quer uma forma fácil de já ter em mãos algumas informações. O JWT (JSON Web Token) é um token assinado que carrega declarações sobre o usuário, as chamadas claims, em um formato que qualquer parte consegue ler. E é comum vê-lo carregando role, permissions, tenantId, userId, organizationId, scopes.
Quando o serviço concede acesso a funções sensíveis ou administrativas apenas porque uma claim de role ou permissão diz que pode, sem revalidar essa decisão no servidor, o OWASP API Security Top 10 classifica o problema como Broken Function Level Authorization (BFLA), quinta posição da lista.
Carregar contexto no token ajuda.
Mas também cria um problema.
JWT vira um lugar muito tentador para carregar contexto demais. E, em alguns sistemas, começa a aparecer de tudo ali: CPF, nome completo, e-mail, telefone, documento, dados de perfil, informações da organização, permissões, flags e atributos que poderiam estar protegidos em outro lugar.
Esse cuidado importa também por uma lente de privacidade e LGPD. JWT muitas vezes trafega em header, pode aparecer em log, pode ser copiado para ferramentas de debug, pode ser inspecionado por times diferentes, pode ficar armazenado no cliente e pode circular por camadas intermediárias.
Mesmo assinado, o conteúdo de um JWT normalmente não é secreto. Assinatura garante integridade. O payload continua legível para qualquer pessoa que tenha o token em mãos, bastando decodificar o Base64.
E aí alguém pode pensar: "então é só criptografar o token".
Não é tão simples. Não existe lanche grátis, alguém sempre está bancando.
Criptografar o conteúdo do token pode fazer sentido em cenários específicos, mas aumenta a complexidade operacional. Agora você precisa cuidar de chaves de criptografia, rotação, compatibilidade entre serviços, debug mais difícil, interoperabilidade com bibliotecas e provedores, além do risco de transformar o token em uma caixa-preta difícil de governar. Em muitos casos, criptografar vira uma tentativa de compensar um problema de design da aplicação que colocou dados demais no token.
O caminho mais saudável costuma ser reduzir o que vai dentro dele. Colocar dado pessoal no token não deveria ser o padrão.
O ideal é carregar apenas o necessário para identificação e decisão de acesso, evitando transformar o token em uma ficha completa do usuário. Quanto mais dado sensível você coloca ali, maior o impacto quando esse token vaza, é logado indevidamente ou é manipulado de forma descuidada pela aplicação cliente.
Outro ponto é que claims são uma fotografia do momento em que o token foi emitido. Só que permissões mudam, usuário muda de área, contrato é cancelado, acesso é revogado, pessoa sai da empresa, tenant é desativado, role é alterada, escopo deixa de valer.
Validar expiração ajuda, mas não resolve tudo.
Um token com expiração curta reduz a janela de risco. Um refresh token bem tratado melhora o controle de sessão. Revogação, introspecção, versionamento de permissão ou rechecagem em ações críticas podem ser necessários dependendo do domínio. Mas apenas colocar exp no JWT não garante que a decisão ainda é válida durante todo o período de vida do token.
Outro ponto básico e extremamente importante: validar assinatura é obrigatório.
sequenceDiagram
participant C as Cliente
participant S as Serviço (Resource Server)
participant IdP as Provedor de Identidade
C->>IdP: Login com credenciais
IdP-->>C: JWT assinado
C->>S: Requisição com JWT no header
S->>IdP: Busca chave pública (endpoint JWKS)
IdP-->>S: Chaves públicas
S->>S: Valida assinatura, emissor e expiração
S->>S: Decide autorização (ação, recurso, contexto)
S-->>C: Resposta (ou 401/403)Em uma aplicação Spring Boot, por exemplo, é comum o serviço atuar como Resource Server OAuth2. Ele recebe o JWT, identifica quem emitiu o token, busca a chave pública no provedor de identidade, geralmente por um endpoint JWKS, e usa essa chave para validar a assinatura. Com isso, o serviço consegue verificar se aquele token foi emitido por uma fonte confiável e se o conteúdo não foi adulterado no caminho.
Mas assinatura não é autorização completa.
Ela prova que o token é íntegro e veio de quem deveria emitir. Ainda falta provar que o usuário pode executar aquela ação naquele recurso específico.
Esse é o ponto central. JWT pode ajudar a transportar contexto, mas não deveria substituir o modelo de autorização da aplicação.
Usar claims é legítimo. O problema está em tratá-las como resposta final para qualquer decisão de acesso.
Isso fica ainda mais evidente quando a claim vira autorização genérica demais.
role = admin
Admin de quê?
De qual organização?
De qual loja?
De qual conta?
Com permissão para qual ação?
Em qual ambiente?
Com qual limite?
Para ações simples, claims bem definidas podem bastar. Para ações críticas, recursos sensíveis, ambientes multi-tenant e permissões mutáveis, o backend precisa validar contexto com mais cuidado.
JWT deveria apoiar a decisão, nunca substituí-la.
O serviço ainda precisa perguntar: essa claim é suficiente para esta ação, sobre este recurso, neste contexto, agora?
O que esses quatro erros têm em comum
Esses quatro problemas parecem diferentes, mas têm a mesma raiz, a confiança. Muitas falhas nascem justamente do excesso dela.
Confia-se no frontend para validar entrada.
Confia-se no frontend para controlar permissão.
Confia-se no token para substituir autorização.
Confia-se na claim para substituir contexto.
Em todos os casos, a aplicação terceiriza uma decisão de segurança para algo que deveria ser apenas parte do fluxo.
Essas práticas são importantes, e a falta delas expõe uma lacuna de engenharia. Segurança precisa aparecer antes de pentest, auditoria, esteira, scanner ou incidente. Precisa estar no repertório básico de quem desenvolve software. Saber validar entrada, modelar autorização, reduzir exposição, proteger dado sensível, pensar em abuso de fluxo e questionar confiança implícita deveria fazer parte do trabalho diário de desenvolvimento.
É aqui que DevSecOps deixa de ser apenas uma sigla ou buzzword.
Assim como DevOps, DevSecOps significa trazer segurança para perto do fluxo de entrega, onde as decisões são tomadas, em vez de criar um cargo, uma pessoa ou uma área que recebe o problema depois. Também está longe de significar jogar mais responsabilidade no desenvolvedor sem contexto, tempo e ferramenta. É prática incorporada ao desenvolvimento, à revisão de código, aos testes, à arquitetura, ao pipeline e à operação, sem virar burocracia no final.
A prática segura é menos glamourosa, mas é parte do trabalho de um profissional de engenharia de software.
Validar no backend.
Autorizar no backend.
Checar ação, recurso e contexto.
Tratar claims como apoio.
Não deixar a interface ser a fronteira de segurança.
Essa é a lógica de shift left, a ideia de mover verificações para o início do fluxo, aplicada na prática. Antecipar segurança evita que decisões inseguras sejam descobertas só quando já estão caras, espalhadas e difíceis de corrigir.
Segurança no desenvolvimento se traduz em pequenas decisões conscientes repetidas todos os dias, muito mais do que em ferramentas.
E uma das mais importantes é simples. Além de pensar no que fazer, pense em quem faz. Não confie em quem apenas deveria informar, facilitar ou transportar contexto.
A decisão de segurança precisa estar onde a regra é protegida.