
O captcha que derruba o login inteiro
Existe uma diferença enorme entre “o captcha está quebrado” e “o captcha está quebrado e eu não sei por quê”. A primeira frase é um incidente. A segunda é um plantão.
Na madrugada de 16 de setembro, o login do PataPass estava devolvendo 403 para todo mundo. Não “às vezes”. Não “para alguns usuários”. Todo mundo. E o detalhe que me tirou o sono não foi o 403 — foi o motivo pelo qual o sistema estava se comportando exatamente como deveria.
O detalhe que me confundiu
O captcha do PataPass estava configurado em modo fechamento por padrão: quando a validação não passa, o acesso é negado. É a escolha correta. Um captcha que falha aberto é um captcha que não existe.
O problema é que esse fechamento transforma qualquer erro de configuração em indisponibilidade total. Se a chave que valida o token some, o sistema não degrada — ele para.
E foi exatamente isso que aconteceu.
Uma linha do arquivo de ambiente da aplicação — a linha lida pelo gerenciador de serviços na hora da inicialização — tinha, desde a tarde anterior, um placeholder no lugar da chave real:
TURNSTILE_SECRET_KEY=COLE_A_SECRET_AQUI
O nome do placeholder dizia tudo. Era o texto que eu mesmo deixo nos arquivos de exemplo, substituído na cópia de trabalho e esquecido no arquivo que foi para produção. Naquela tarde, alguém — ou alguma rotina de sincronização — descomentou aquela linha para testar. Na reinicialização seguinte, o fechamento por padrão pegou o placeholder, tentou validar, não conseguiu, e passou a recusar todo login e todo cadastro com 403.
Nove horas de indisponibilidade. O sistema nunca esteve mais correto do que estava: ele fez exatamente o que mandaram fazer.
A parte realmente incomoda: a chave não existia
Se o placeholder fosse a única versão ruim, o conserto seria escrever a chave certa e pronto. Não foi.
Quando fui procurar a chave real, fiz a varredura que se faz quando se herda uma configuração de meses: disco inteiro nos dois ambientes, histórico de comandos, histórico do git, backups, variáveis de ambiente, metadados do provedor de integração contínua, e as doze credenciais de nuvem da máquina testadas uma a uma.
O resultado foi negativo em todos os lugares. A chave não existia em nenhum lugar acessível.
Este é o detalhe que me fez parar e escrever esta história. Se a chave nunca existiu, quem validava o captcha antes? Resposta: ninguém, de forma confiável. O front-end publicado estava com a chave de teste do provedor de captcha — que, por projeto, aprova qualquer desafio. O back-end “validava” contra uma credencial que não tinha como validar.
Ou seja: havia um captcha que não captchava, apoiado por um back-end que conferia uma porta destrancada. Só que o back-end acreditava estar conferindo uma porta trancada. E, quando alguém corrigiu a configuração para que a validação passasse a falhar de verdade, o sistema inteiro sentiu.
Um controle de segurança que nunca exercitou o caminho de erro não tem caminho de erro. Tem apenas o momento em que alguém o exercita — e esse momento é sempre em produção, quase sempre de madrugada.
O diagnóstico que não vê o segredo
O primeiro instinto é imprimir a configuração. É o instinto errado.
A regra que eu segui, e que quero registrar aqui porque economizou horas: o diagnóstico de um segredo nunca precisa ver o segredo. Ele precisa ver o resultado de usá-lo.
Provei o par chamando o endpoint de validação com a chave e com um token deliberadamente falso, e observei apenas os códigos de erro devolvidos:
# resposta propositalmente invalida: nao testa o captcha, testa o PAR
curl -s -m 20 https://challenges.cloudflare.com/turnstile/v0/siteverify \
-d "secret=$SECRET" \
-d "response=doctor-fake-par-check"
E aqui está o truque que eu não conhecia: os códigos de erro separam dois mundos que parecem iguais à distância.
invalid-input-response— o par está bom. A chave existe, foi aceita, e o sistema avançou até o ponto de rejeitar o token falso. Isso é uma prova de vida.invalid-input-secret— a chave morreu. O provedor nem chegou a olhar o token.
Um teste com token falso deveria falhar. Quando ele falha do jeito certo, isso é a melhor notícia do dia. O erro é o sinal de vida.
Rodei isso com a credencial que estava na integração contínua. Resultado: invalid-input-secret. A chave de lá estava morta no provedor — e, por acaso, era de um formato que não batia com o formato oficial, o que explicava por que ninguém tinha percebido antes: ela nunca funcionou.
O doctor que ficou para depois
Com o diagnóstico na mão, escrevi duas ferramentas — uma que roda na integração contínua e outra que roda localmente — com uma regra única: nenhuma delas imprime, grava no log ou devolve o valor do segredo. Elas imprimem apenas comprimento, prefixo de hash e códigos de resposta.
O script local faz algo a mais: ele lista os widgets de captcha da conta, encontra aquele cujo par público bate com o que a produção realmente usa, e só então grava a credencial no arquivo de ambiente — e só se três condições forem verdadeiras ao mesmo tempo:
- o formato da credencial bate com o esperado;
- a validação de par responde
invalid-input-response, ou seja, par válido; - o arquivo de ambiente tem exatamente uma linha âncora para aquela chave.
Cada uma dessas condições aborta a operação sem gravar. A terceira parece exagero, mas é a que evita o pior erro possível nesta situação: gravar a chave no lugar errado e continuar com um arquivo de ambiente que tem duas entradas conflitantes — que é como se ganha um defeito que só aparece em um ambiente específico, na máquina de alguém, às três da manhã.
E o backup é feito antes, com nome explícito. Não é exagero. É o motivo de você ter uma chance de voltar atrás.
A regra que sobrou
Depois de consertar, eu escrevi uma regra na documentação do projeto que é curta e não vou parafrasear:
Nunca escrever a chave do captcha no arquivo de ambiente sem que a validação de par tenha confirmado o par antes da reinicialização.
A razão é simétrica com o incidente. Chave ativa e inválida é login morto. Chave comentada é verificação desligada. Não existe configuração intermediária que seja segura por acidente — só existe a configuração que foi verificada antes de virar produção.
Verifiquei ao vivo depois da correção: com a chave no lugar, o endpoint passou a devolver 401 para credencial errada e 400 para senha que não passa na validação de formato. Erro de verdade, no nível certo, com a resposta certa. Essa é a diferença entre um sistema que falha e um sistema que funciona.
O que eu levo disso
Três coisas, e nenhuma delas é sobre captcha.
Primeira: um controle de segurança só vale o caminho de erro que ele exercita. Um verificador que nunca viu um token falso é um enfeite. O teste do par falso é o que transforma o controle em controle.
Segunda: diagnóstico sem segredo é uma habilidade, não um truque. Quem precisa ver o valor para diagnosticar constrói sistemas que vazam. Quem precisa só do código de retorno constrói sistemas que podem ser diagnosticados por qualquer pessoa, em qualquer lugar, com qualquer permissão — inclusive a de não ter nenhuma.
Segunda, continuação: erro de configuração é incidente de disponibilidade, não de segurança. O fechamento por padrão estava certo. Ele só estava configurado com o valor errado. Quando um controle de segurança derruba o produto, a primeira pergunta não é “como burlamos o controle” — é “qual valor o controle está lendo”.
Terceira: a lista de fontes para procurar um segredo é finita e eu a decorei. Disco, histórico de comandos, histórico do git, backups, variáveis de ambiente, metadados da integração contínua, credenciais da conta. Quando todas as sete ficam negativas, a conclusão não é “procure mais” — é “esse valor provavelmente nunca foi gravado”, e o conserto é criar, não recuperar.
O login voltou antes do amanhecer. O captcha real, com validação real, entrou depois — quando alguém autorizou a criação do widget certo. Mas o incidente já tinha ensinado o que eu precisava saber, e isso não dependia de permissão nenhuma.