El curioso caso del punto desaparecido
(tjaart.substack.com)- En un sistema que generaba emails, mensajes de texto y PDF a partir de plantillas de documentos, apareció un problema en el que desaparecía un punto solo en el cuerpo de los emails enviados a ciertos clientes
- El punto estaba en la plantilla original, en la vista previa y en el PDF, pero el problema solo se reproducía con cierta longitud de línea después de reemplazar los valores de placeholder específicos de cada cliente por datos reales
- Un cliente SMTP personalizado limitaba la longitud de las líneas y empujaba el punto al primer carácter de una nueva línea; debido a la regla de dot transparency de SMTP, el servidor eliminaba ese punto
- La corrección consistió en que el cliente agregara un punto adicional al inicio de las líneas que empezaran con un punto y tuvieran otros caracteres después
- Otro equipo que compartía el mismo código del cliente SMTP no aplicó el parche, por lo que se enviaron emails en los que, para algunos clientes, la prima mensual del seguro se veía como $2700 en lugar de $27.00; el bug se corrigió de inmediato
El problema que empezó en un sistema central de plantillas
- Hace unos 7 años, un cliente quería consolidar en un solo sistema documentos dispersos en varias plantillas de Microsoft Word
- El método anterior consistía en que los empleados reemplazaran manualmente los placeholders del documento por nombres, apellidos, etc.; y empezaron a circular plantillas con términos y condiciones antiguos, logos viejos de la empresa y fuentes incorrectas, lo que volvió difícil administrarlas
- El nuevo sistema administraba las plantillas de documentos de forma centralizada y, a partir de ellas, generaba documentos PDF, mensajes de texto y cuerpos de email
- El mensaje de bienvenida para nuevos clientes también podía tener plantillas distintas según el canal de entrega
- La versión para email podía usar tablas HTML y estilos básicos
- La versión para envío postal podía incluir infografías
- La versión para mensaje de texto solo podía contener un breve texto de bienvenida
El punto que desaparecía solo para un cliente específico
- Varios meses, o quizá más de un año después de que el sistema entrara en operación, un administrador avisó que faltaba un punto en el cuerpo de un email enviado a un cliente específico
- Si se enviaba el mismo email a otro cliente, el punto no desaparecía, por lo que era difícil considerarlo un simple error de plantilla
- En el código fuente de la plantilla, ese punto efectivamente existía
- Al copiar la plantilla de producción en el entorno local y generar una vista previa del cuerpo del email, el punto también aparecía; la versión para impresión creada con la misma plantilla también se mostraba correctamente
- En el entorno local, los emails se enviaban a localhost en un puerto específico y se revisaban con un servidor SMTP falso como SMTP4dev y Outlook
- Incluso al revisar en Outlook el primer email enviado localmente, el punto se mostraba correctamente
Las condiciones de reproducción creadas por los datos de cada cliente
- Al crear emails, PDF y mensajes de texto, la plantilla reemplazaba los valores de placeholder, como nombre y apellido del cliente, por datos reales
- Aunque fuera la misma plantilla, el cuerpo del email enviado a cada cliente podía tener longitudes de contenido distintas
- Al buscar los valores de placeholder usados realmente para el cliente afectado y reenviar el email en el entorno local con esos mismos valores, se confirmó en Outlook que el punto desaparecía
- El problema dependía de la longitud y el contenido concretos del cuerpo del email de ese cliente
- Al principio se revisó si el carácter de punto en la plantilla estaba codificado, o si no era un punto real sino otro carácter, pero esa no era la causa
- Al mover la posición del punto una ubicación dentro de la plantilla, el punto volvió a verse en Outlook, y la posición de la línea se convirtió en una pista para reproducir el problema
Longitud de línea en SMTP y dot transparency
- La depuración mostró que el código que guardaba el email en la base de datos no modificaba la plantilla, salvo por reemplazar los placeholders por la información del cliente
- Luego, el alcance de la investigación se redujo al código del trabajo cron que obtenía periódicamente los destinatarios y enviaba los emails
- Parte del código que llamaba el trabajo cron se había traído de un proyecto anterior e incluía una implementación personalizada de cliente SMTP
- Ese código tenía una función que dividía las líneas para que cada línea del cuerpo del email no superara cierta cantidad de caracteres
- Este comportamiento está relacionado con el límite de longitud de línea de la especificación SMTP
- La longitud total máxima de una línea de texto es de 1000 octets, incluido
<CRLF> - El punto inicial duplicado para lograr transparencia no se incluye en este cálculo
- Con SMTP Service Extensions, este valor puede aumentar
- La longitud total máxima de una línea de texto es de 1000 octets, incluido
- En el cuerpo del email afectado, el límite de longitud de línea hizo que el punto pasara a ser el primer carácter de la siguiente línea
- En la especificación SMTP, el indicador de fin de mail data se maneja como una línea que contiene solo un punto
- La regla de dot transparency de SMTP maneja de forma especial las líneas del cuerpo que empiezan con un punto
- Antes de enviar una línea de texto del correo, el cliente SMTP debe verificar si el primer carácter es un punto; si lo es, debe insertar un punto adicional al inicio de la línea
- Si una línea contiene solo un punto, el servidor SMTP la trata como el fin de los datos del correo
- Si el primer carácter es un punto y hay otros caracteres en la misma línea, el servidor elimina el primer carácter
- El cliente SMTP personalizado no hacía el procesamiento de agregar un punto adicional cuando una línea empezaba con un punto, y el servidor SMTP receptor eliminaba el primer punto, haciendo que desapareciera el punto real del cuerpo
La corrección y el impacto que salió a la luz más tarde
- La corrección consistió en que el cliente agregara un punto más cuando una línea empezara con un punto y tuviera otros caracteres en la misma línea
- Al enviarlo así, aunque el servidor SMTP receptor eliminara el primer punto, el punto original quedaba en el cuerpo
- Al reenviar localmente el email original con la misma información del destinatario del cliente afectado, el punto ya no desapareció
- Después de desplegar la corrección, como el mismo código de cliente SMTP se había reutilizado desde un proyecto anterior, se avisó del bug a otro equipo
- Unos meses después, el sistema de ese otro equipo seguía sin parchear y envió varios emails importantes para informar a los clientes sobre su nueva prima mensual de seguro
- En algunos emails, el punto separador decimal de la prima mensual quedó exactamente en la posición del primer carácter de una línea y desapareció
- Como resultado, algunos clientes recibieron emails en los que la nueva prima parecía ser
$2700y no$27.00 - Este bug dependía de la longitud de cada línea del cuerpo del email, y solo les ocurrió a algunos clientes cuya longitud de nombre y apellido coincidía exactamente
- Como la causa ya estaba identificada, el código de ese equipo se parcheó de inmediato
2 comentarios
Parece que en el título interpretaron
periodcomo período y no como punto final, jaja.Opiniones de Hacker News
Parte de este código estaba implementando directamente un cliente SMTP, y la causa raíz parece estar ahí.
Implementar un protocolo correctamente es difícil, y errores como el del artículo son comunes.
Una biblioteca de cliente SMTP bien implementada habría codificado el texto de entrada conforme a las reglas de SMTP sin importar dónde estuvieran los puntos, y la capa de plantillas no tendría que preocuparse por SMTP.
Recibir texto y codificarlo según las reglas de SMTP no es difícil, pero hay que saber desde el principio que ese procesamiento es necesario.
Muchísimos errores y vulnerabilidades de seguridad, como inyección SQL y XSS, nacen del mismo error: concatenar strings.
En una consulta SQL, vincular valores a una plantilla de consulta debe ocurrir en el “espacio SQL”, no en un espacio de strings sin tipos; construir directamente un string SQL serializado como
SELECT * FROM foo WHERE foo.bar =+$userDataes la forma incorrecta.Lo mismo aplica a las plantillas HTML: si se manejan a nivel de árbol del documento y no como una representación en string, se pueden evitar vulnerabilidades tontas.
Para evitar que desaparezcan puntos en un email, no hay que inyectar texto sin estructura en medio del pipeline SMTP, sino respetar el nivel de abstracción en el que se está trabajando.
En relación con esto, también vale la pena ver langsec.
Como definió “una línea que contiene solo un punto” como una secuencia de control y no como una línea literal, pasan estas cosas.
SMTP es un ejemplo de diseño innecesariamente complejo, y los errores de implementación reflejan esa complejidad.
No digo que haya que recomendar implementarlo por cuenta propia, pero SMTP no debería ser tan difícil de implementar correctamente incluso en solitario.
El resto es peso muerto, pero por funciones que ni siquiera se llaman arrastra 20 dependencias más, y de pronto la base de código se infla varios MB y empiezas a recibir alertas de CVE de una biblioteca que ni sabías que usabas.
La parte técnica ya la trataron mejor otros, pero esto me recuerda una anécdota sobre lo importante que puede ser algo tan pequeño como un punto final al final de una oración.
En Alemania, donde trabajo, al dejar un empleo es común solicitar un “Zeugnis”, una carta de recomendación que describe las tareas realizadas y evalúa al empleado, y suele ser un documento importante que se pide al buscar trabajo.
Obviamente, un empleado no aceptaría una frase como “esta persona es floja, no la contraten”, así que surgió una especie de código llamado “Zeugnissprache”, disfrazado de elogios.
Una de esas claves dice que si la última oración no tiene punto final significa “ignoren todo lo escrito aquí; esta persona es terrible”.
Después de mi último trabajo hice que un abogado revisara mi Zeugnis y, aunque todas las evaluaciones eran positivas, quizá por descuido faltaba el punto final en la última oración.
Los responsables de RR. HH. no tienen incentivos para crear un código secreto que usarían junto con posibles competidores, y tampoco tiene sentido que lo enseñen a nuevos responsables de RR. HH. y a la vez lo mantengan en secreto.
Estas leyendas surgen porque RR. HH. no puede escribir demasiado explícitamente que alguien fue un dolor de cabeza.
Si no hay nada positivo que decir, elogian virtudes comunes como la puntualidad; más que un código secreto, es algo parecido al “bless their heart” del lenguaje de RR. HH.
Si hay una demanda, les deja margen para zafarse diciendo: “¡Señoría, era algo bueno! ¡Siempre llegaba a tiempo!”.
No entiendo por qué una tarea de cron que envía emails tendría que implementar su propio cliente SMTP.
Bastaría con usar un programa como
mailde mailutils.Incluso desde el punto de vista de la entregabilidad, armar por cuenta propia una interacción SMTP mínima sobre un socket es una mala idea desde el inicio.
Hoy en día no puedes simplemente conectarte a cualquier host de intercambio de correo para enviar email; normalmente tienes que conectarte a un host de relay SMTP específico provisto por el ISP.
Para eso tendrías que implementar conexión TLS, autenticación, etc.
Lo un poco irónico es que cron en sí ya sabe enviar correo.
La salida de una tarea de cron se envía por email al propietario, y algunas implementaciones de cron permiten cambiar la dirección de destino con algo como la variable
MAILTOen el crontab.La razón es que un usuario que quiere usar “esa cosa” probablemente puede escribir la dirección de un servidor SMTP, pero el programa no tiene forma de confiar en que un MTA de envío como sendmail esté configurado correctamente.
En las empresas existen a gran escala servidores que no pueden enviar correo del sistema, y muchas veces ese estado queda fuera del control del programador o del usuario.
La configuración de correo es un área especializada que requiere prerrequisitos como DNS, cifrado y políticas, así que es demasiado para el simple objetivo de hacer que algo como una app de billetera envíe emails.
“Contacta al administrador del sistema” muchas veces no es una opción realista, ni a gran ni a pequeña escala.
No es una buena razón, pero sí es la razón real.
En casi cualquier lenguaje hay muchas bibliotecas de cliente SMTP utilizables, aunque no estén en la biblioteca estándar.
Aquí se ven dos malos hábitos importantes
El primero, como muchos señalaron, es no implementar los estándares a medias
Si tienes que implementarlos tú mismo, hay que poner el cuidado y la dedicación necesarios, o usar una biblioteca ya hecha
El segundo es no hacer vendoring de las dependencias
Las bibliotecas que usas deben actualizarse de forma regular y a tiempo, no “solo cuando haga falta”
Si las actualizaciones se retrasan o directamente se evitan, incluso bugs que upstream ya corrigió pueden convertirse en un gran problema para quienes creen que solo hay que actualizar cuando el problema se vuelve visible para ellos
Entonces la estabilidad de la aplicación queda expuesta a cambios upstream, y esos cambios pueden romper el código
Puede que no reciba las correcciones de inmediato, pero prefiero saber que estoy cambiando por una corrección que necesito antes que introducir inestabilidad no deseada y riesgos adicionales
Soy más de “si no está roto, no lo arregles”
Hoy en día parece que mucha gente no aprende los protocolos básicos interactuando directamente con una terminal
SMTP parece haber sido pensado originalmente para ese tipo de interacción manual, y como alguien que lo usó así durante un tiempo, lo de “una línea con un solo punto” para terminar un mensaje se me quedó grabado para siempre
En relación con eso, el escape también parece ser un concepto ajeno para muchos programadores
Al ver la situación anterior, hay mucha gente que no se pregunta “¿qué pasa si quiero enviar un correo con una línea que solo contiene un punto?”, pero otro grupo grande encontrará esa pregunta muy lógica y fácil de entender
Hace falta dot stuffing
SMTP https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
También existe en POP3 https://www.rfc-editor.org/rfc/rfc1939#page-8
Me recordó a una experiencia depurando la implementación de un protocolo de red. En concreto, AppleTalk NBP, para los veteranos
Ya tenía todo codificado, pero mis paquetes eran rechazados, es decir, descartados silenciosamente, mientras que los paquetes de la implementación real de Apple sí pasaban
Puse en la pantalla de la computadora un paquete bueno y uno malo y los comparé byte por byte, pero no encontraba el problema; eran exactamente iguales de principio a fin, incluido el checksum
Llegó la hora de salir del trabajo y decidí imprimir esas tonterías para revisarlas después; en cuanto las imprimí, vi el error
Mi versión ocupaba dos páginas, y la implementación correcta una sola
No estaba vaciando correctamente el búfer antes de enviar los datos. Así son los mbuf
Todavía me río cuando lo recuerdo
La línea “We are happy to welcome you to our family.” ni siquiera se acerca al límite de longitud de línea
Parece que pasaba algo más; por ejemplo, quizá todo era en realidad un adjunto MIME HTML
Podría haber sido algo como
... lots of text ...seguido deWe are happy to welcome you to our family.Pero si cortas HTML arbitrariamente en líneas, rompes las etiquetas
Es muy probable que la empresa ni se entere de ese pequeño problema
quoted-printable debería tener un límite máximo de 78 caracteres incluyendo CRLF, pero los clientes de correo suelen ser bastante tolerantes
Si es la primera vez que oyes hablar de dot stuffing, te costará creer qué otros horrores existen en el mundo del email
Cosas como el plegado de encabezados, el manejo de comillas en la parte local y los literales IPv6
Apenas leí que era código de cliente SMTP tomado de un proyecto anterior, pensé que el final sería que otro equipo todavía no había parcheado este bug