O curioso caso do ponto final desaparecido
(tjaart.substack.com)- Em um sistema que gerava e-mails, mensagens de texto e PDFs a partir de modelos de documentos, surgiu um problema em que pontos finais desapareciam apenas no corpo de e-mails enviados a determinados clientes
- O ponto final existia no modelo original, na pré-visualização e no PDF, mas o problema só era reproduzido em um comprimento específico de linha depois que os valores de placeholder de cada cliente eram substituídos pelos dados reais
- Um cliente SMTP customizado limitava o comprimento das linhas e empurrava o ponto final para o primeiro caractere de uma nova linha; por causa da regra de dot transparency do SMTP, o servidor removia esse ponto
- A correção foi feita fazendo o cliente acrescentar mais um ponto antes de linhas que começam com ponto e têm outros caracteres depois
- Outra equipe que compartilhava o mesmo código do cliente SMTP não aplicou o patch, e e-mails em que o prêmio mensal de seguro aparecia como $2700 em vez de $27.00 foram enviados a alguns clientes; o bug foi corrigido imediatamente
O problema que começou no sistema central de modelos
- Cerca de 7 anos atrás, um cliente queria consolidar em um único sistema documentos espalhados por vários modelos do Microsoft Word
- O método existente fazia funcionários substituírem manualmente os placeholders do documento por nome, sobrenome etc.; modelos com termos antigos, logos antigos da empresa e fontes incorretas circulavam juntos, tornando a gestão difícil
- O novo sistema foi configurado para gerenciar modelos de documentos centralmente e, com base neles, gerar documentos PDF, mensagens de texto e corpos de e-mail
- A mensagem de boas-vindas para novos clientes também podia ter modelos diferentes conforme o canal de entrega
- A versão por e-mail podia usar tabelas HTML e estilos básicos
- A versão para envio postal podia incluir infográficos
- A versão por mensagem de texto podia conter apenas uma breve mensagem de boas-vindas
O ponto final que desaparecia só para um cliente específico
- Alguns meses, ou talvez mais de um ano depois que o sistema entrou em operação, um administrador informou que um ponto final estava faltando no corpo de um e-mail enviado a um cliente específico
- Ao enviar o mesmo e-mail para outros clientes, o ponto final não desaparecia, então era difícil tratar o caso como um simples erro de modelo
- O ponto final realmente existia no código-fonte do modelo
- Em um ambiente local, ao copiar o modelo de produção e gerar uma pré-visualização do corpo do e-mail, o ponto aparecia; a versão para impressão criada com o mesmo modelo também era exibida corretamente
- O ambiente local enviava e-mails para localhost em uma porta específica e verificava as mensagens com um servidor SMTP falso, como o SMTP4dev, e o Outlook
- Mesmo ao verificar no Outlook o primeiro e-mail enviado localmente, o ponto final era exibido normalmente
As condições de reprodução criadas pelos dados de cada cliente
- Ao criar e-mails, PDFs e mensagens de texto, o modelo substituía valores de placeholder, como nome e sobrenome do cliente, pelos dados reais
- Mesmo usando o mesmo modelo, o corpo do e-mail enviado a cada cliente podia ter comprimentos diferentes
- Ao encontrar os valores de placeholder realmente usados para o cliente afetado e reenviar localmente o e-mail com esses mesmos valores, foi confirmado no Outlook que o ponto final desaparecia
- O problema dependia do comprimento e conteúdo específicos do corpo daquele e-mail de cliente
- No início, foram verificados aspectos como se o caractere de ponto no modelo estava codificado ou se não era um ponto real, mas essa não era a causa
- Ao mover a posição do ponto final dentro do modelo em um caractere, o ponto voltou a aparecer no Outlook, e a posição da linha virou uma pista para a reprodução
Comprimento de linha no SMTP e dot transparency
- A depuração mostrou que o código que salvava o e-mail no banco de dados não alterava o modelo além de substituir placeholders por informações do cliente
- Depois disso, o escopo da investigação foi reduzido ao código do job cron que buscava periodicamente os destinatários e enviava os e-mails
- Parte do código chamado pelo job cron vinha de um projeto anterior e incluía uma implementação customizada de cliente SMTP
- Nesse código havia uma função que quebrava as linhas para que cada linha do corpo do e-mail não passasse de um determinado número de caracteres
- Esse comportamento está relacionado ao limite de comprimento de linha da especificação SMTP
- O comprimento total máximo de uma linha de texto é de 1000 octets, incluindo
<CRLF> - O ponto inicial duplicado para fins de transparência não é incluído nesse cálculo
- Esse valor pode ser aumentado por SMTP Service Extensions
- O comprimento total máximo de uma linha de texto é de 1000 octets, incluindo
- No corpo do e-mail problemático, por causa do limite de comprimento de linha, o ponto final foi movido para o primeiro caractere da linha seguinte
- Na especificação SMTP, o indicador de fim dos dados do e-mail é tratado como uma linha contendo apenas um ponto
- A regra de dot transparency do SMTP trata separadamente linhas do corpo que começam com ponto
- Antes de enviar uma linha de texto do e-mail, o cliente SMTP deve verificar se o primeiro caractere é um ponto; se for, deve inserir mais um ponto no início da linha
- O servidor SMTP trata uma linha contendo apenas um ponto como o fim dos dados do e-mail
- Se o primeiro caractere for um ponto e houver outros caracteres na mesma linha, o servidor remove o primeiro caractere
- O cliente SMTP customizado não fazia o processamento de acrescentar mais um ponto quando uma linha começava com ponto, e o servidor SMTP receptor apagava o primeiro ponto, fazendo o ponto real desaparecer do corpo
Correção e impacto revelado tardiamente
- A correção consistiu em fazer o cliente acrescentar mais um ponto quando a linha começava com ponto e havia outros caracteres na mesma linha
- Enviando assim, mesmo que o servidor SMTP receptor remova o primeiro ponto, o ponto original permanece no corpo
- Ao reenviar localmente o e-mail original com as mesmas informações do destinatário do cliente afetado, o ponto final não desapareceu mais
- Depois que a correção foi implantada, outra equipe foi avisada do bug, pois também usava o mesmo código de cliente SMTP trazido de um projeto anterior
- Alguns meses depois, o sistema dessa outra equipe ainda não tinha sido corrigido e enviou vários e-mails importantes informando aos clientes o novo prêmio mensal de seguro
- Em alguns e-mails, o ponto separador decimal do prêmio mensal ficou exatamente na posição do primeiro caractere da linha e desapareceu
- Como resultado, alguns clientes receberam e-mails em que o novo prêmio parecia ser
$2700, não$27.00 - Esse bug dependia do comprimento de cada linha no corpo do e-mail e só ocorria para alguns clientes cujo comprimento do nome e sobrenome coincidia exatamente
- Como a causa já havia sido identificada, o código dessa equipe foi corrigido imediatamente
2 comentários
Parece que no título interpretaram
periodcomo período, e não como ponto final hahaComentários do Hacker News
Parte desse código estava implementando diretamente um cliente SMTP, e a causa raiz parece estar aí
Implementar um protocolo com precisão é difícil, e bugs como o do texto são comuns
Uma biblioteca de cliente SMTP bem implementada teria codificado o texto de entrada de acordo com a especificação do SMTP independentemente da posição dos pontos no texto, e a camada de template não precisaria se preocupar com SMTP
Receber texto e codificá-lo conforme a especificação do SMTP não é difícil, mas é preciso saber, antes de tudo, que esse tratamento é necessário
Muitos erros e vulnerabilidades de segurança, como injeção de SQL e XSS, surgem do mesmo erro: concatenação de strings
Em consultas SQL, o vínculo dos valores ao template da query deve acontecer no “espaço SQL”, não em um espaço de strings sem tipos, e montar diretamente uma string SQL serializada como
SELECT * FROM foo WHERE foo.bar =+$userDataé a forma errada de fazer issoCom templates HTML acontece o mesmo: se você trabalhar no nível da árvore do documento em vez da representação em string, evita vulnerabilidades bobas
Para evitar que pontos desapareçam em emails, não injete texto sem estrutura no meio do pipeline SMTP e respeite o nível de abstração em que você está trabalhando
Vale a pena olhar também para langsec
Como ele define uma “linha contendo apenas um ponto” não como uma linha literal de texto, mas como uma sequência de controle, esse tipo de coisa acontece
O SMTP é um exemplo de design desnecessariamente complexo, e bugs de implementação refletem essa complexidade
Não estou recomendando implementar isso na mão, mas o SMTP também não deveria ser algo tão difícil de implementar corretamente sozinho
O resto é peso morto, mas ainda assim puxa mais 20 dependências por causa de funções que nem são chamadas, e de repente a base de código incha vários MB, enquanto você começa a receber alertas de CVE de bibliotecas que nem sabia que estava usando
Os aspectos técnicos já foram tratados melhor por outras pessoas, mas isso me lembrou uma história sobre como algo aparentemente pequeno, como um ponto final no fim de uma frase, pode ser importante
Na Alemanha, onde eu trabalho, é comum pedir um “Zeugnis” ao sair de um emprego, uma carta com as atividades desempenhadas e a avaliação do funcionário, e isso costuma ser um documento importante na busca por trabalho
Naturalmente, nenhum funcionário aceitaria uma frase como “essa pessoa é preguiçosa, não a contratem”, então surgiu uma espécie de código chamado “Zeugnissprache”, disfarçado de elogio
Um desses códigos seria que, se a frase final não tiver ponto final, isso quer dizer “ignore tudo o que está escrito aqui, essa pessoa é péssima”
Depois do meu emprego anterior, pedi para um advogado revisar meu Zeugnis, e todas as avaliações eram positivas, mas por descuido o ponto final da última frase estava faltando
O pessoal de RH não tem incentivo para criar um código secreto compartilhado com potenciais concorrentes, e também não faz sentido imaginar que isso seria mantido em segredo enquanto é ensinado a novos profissionais de RH
Essas lendas surgem porque o RH não pode escrever de forma direta demais que alguém era problemático
Quando não há nada realmente positivo para dizer, acabam elogiando vantagens banais, como pontualidade; é menos um código secreto e mais algo próximo do “bless their heart” corporativo
Em caso de processo, isso deixa a saída pronta: “Excelência, foi um elogio! Afinal, ele sempre chegava no horário!”
Não entendo por que um job de cron que envia emails precisaria implementar o próprio cliente SMTP
Era só usar um programa como o
maildo mailutilsMesmo do ponto de vista de entregabilidade, montar na mão uma interação SMTP mínima sobre um socket já começa mal
Hoje em dia você não consegue simplesmente se conectar diretamente a qualquer host de troca de email para enviar mensagens; normalmente precisa se conectar a um host de relay SMTP específico fornecido pelo ISP
Para isso, seria preciso implementar também conexão TLS, autenticação e tudo mais
A leve ironia é que o próprio cron já sabe enviar email
A saída de jobs do cron é enviada por email ao dono, e em alguns crons dá até para mudar o endereço de destino com algo como a variável
MAILTOno crontabO motivo é que o usuário que quer usar “aquele negócio” talvez consiga informar o endereço de um servidor SMTP, mas o programa não tem como confiar que um MTA de saída, como sendmail, esteja configurado corretamente
Em empresas existem muitos servidores que simplesmente não conseguem enviar email de sistema, e muitas vezes isso está fora do controle tanto do autor do programa quanto do usuário
Configuração de email é uma área especializada, com pré-requisitos de DNS, criptografia, políticas e afins, então é exagerado exigir tudo isso só para permitir que algo como um app de carteira envie email
“Fale com o administrador do sistema” muitas vezes não é uma opção realista, seja em ambientes grandes ou pequenos
Não é um bom motivo, mas é o motivo real
Em praticamente qualquer linguagem há várias bibliotecas cliente SMTP utilizáveis, mesmo que não estejam na biblioteca padrão
Dá para ver dois maus hábitos grandes aqui
O primeiro, como muita gente já apontou, é: não implemente padrões de qualquer jeito
Se precisar implementar você mesmo, faça isso com o cuidado e a atenção necessários, ou use uma biblioteca já pronta
O segundo é não fazer vendorização de dependências
As bibliotecas que você usa devem ser atualizadas regularmente e no tempo certo, não apenas “quando for preciso”
Se as atualizações forem adiadas ou simplesmente evitadas, até bugs já corrigidos no upstream podem se tornar um grande problema para quem acha que só deve atualizar quando o problema aparecer diretamente
Nesse caso, a estabilidade da aplicação fica exposta a mudanças no upstream, e essas mudanças podem quebrar o código
Talvez você não receba correções imediatamente, mas eu acho melhor saber que estou mudando algo por causa de uma correção de que preciso do que introduzir instabilidade indesejada e risco extra
Sou do time do “se não está quebrado, não conserte”
Hoje em dia, parece que muita gente aprende sem interagir diretamente pelo terminal com os protocolos básicos
O SMTP claramente parece ter sido pensado para esse tipo de interação manual, e, como alguém que realmente usou assim por um tempo, tenho permanentemente gravado na memória o “um único ponto em uma linha” que encerra a mensagem
Nesse contexto, o tratamento de escape também parece ser um conceito estranho para muitos programadores
Muita gente olha para essa situação e não pergunta “e se eu quiser enviar um e-mail com uma linha que contenha só um ponto?”, mas outro grupo grande vai achar essa pergunta extremamente lógica e fácil de entender
É preciso dot-stuffing
SMTP https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
Também existe no POP3 https://www.rfc-editor.org/rfc/rfc1939#page-8
Isso me lembrou de uma vez em que depurei a implementação de um protocolo de rede. Especificamente, era o AppleTalk NBP para os mais antigos
Codifiquei tudo, mas meus pacotes eram rejeitados, isto é, descartados silenciosamente, enquanto os pacotes da implementação real da Apple passavam
Coloquei o pacote bom e o ruim na tela do computador e comparei byte por byte, mas não consegui encontrar o problema, e do início ao fim estava tudo exatamente igual, inclusive o checksum
Já estava na hora de ir embora, então resolvi imprimir aquelas porcarias para olhar depois, e, assim que imprimi, o erro ficou óbvio
A minha versão tinha duas páginas, e a implementação correta tinha uma só
Eu não tinha esvaziado o buffer corretamente antes de enviar os dados.
mbufé assim mesmoAinda rio quando lembro disso
A linha “We are happy to welcome you to our family.” nem chega perto do limite de comprimento de linha
Parece que há outra coisa acontecendo, por exemplo o conteúdo inteiro pode na verdade ter sido um anexo MIME em HTML
Pode ter sido algo como
... lots of text ...seguido deWe are happy to welcome you to our family.Mas, se você simplesmente cortar HTML em linhas, as tags quebram
É bem provável que a empresa nunca perceba esse pequeno problema
O quoted-printable deve ter limite máximo de 78 caracteres, incluindo CRLF, mas os clientes de e-mail em geral costumam ser tolerantes
Para quem nunca ouviu falar de dot-stuffing, pode ser difícil acreditar que existam outros horrores no mundo do e-mail
Há coisas como folding de cabeçalhos, tratamento de aspas na parte local e literais IPv6
Assim que li que era código de cliente SMTP emprestado de um projeto anterior, já imaginei que a história terminaria com a outra equipe ainda não tendo corrigido esse bug