1 puntos por GN⁺ 2024-05-22 | 2 comentarios | Compartir por WhatsApp
  • 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
  • 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 $2700 y 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

 
surfindia 2024-05-22

Parece que en el título interpretaron period como período y no como punto final, jaja.

 
GN⁺ 2024-05-22
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.

    • El verdadero problema no está tanto en el protocolo en sí, sino en el enfoque cowboy para manejarlo.
      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 = + $userData es 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.
    • El verdadero problema es que SMTP es un protocolo de “texto plano” con señalización en banda.
      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.
    • Suena como una buena idea, pero tal vez incorporas una biblioteca SMTP y esa biblioteca arrastra 5 dependencias, cada una de las cuales arrastra otras 3 dependencias transitivas, y una de ellas puede ser en realidad un proyecto caja de herramientas universal del que solo se usa el 1%.
      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.
    • https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law
  • 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.

    • Eso de que las cartas de recomendación usan códigos secretos es una leyenda urbana.
      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!”.
    • “Si la última oración no tiene punto final, ignoren todo lo escrito aquí; esta persona es terrible”. Vaya, ¿qué podría salir mal?
    • Me pregunto si revisaste si también faltaban puntos en los documentos de evaluación de desempeño.
  • 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 mail de 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 MAILTO en el crontab.

    • Aunque se debería usar el MTA del host para enviar correo, en la práctica hay muchas cosas que traen integrado su propio cliente SMTP.
      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.
    • No existe un mundo en el que puedas confiar en que el sistema ya tiene un binario que enviará correo por ti, pero aun así yo no lo implementaría desde cero.
      En casi cualquier lenguaje hay muchas bibliotecas de cliente SMTP utilizables, aunque no estén en la biblioteca estándar.
    • Esto también parece un caso de la ley de Zawinski.
  • 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

    • La alternativa a no hacer vendoring de dependencias me parece peor
      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

    • Me sorprendería si en los últimos 30 años una proporción significativa de desarrolladores de software hubiera aprendido de esa manera
  • 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 de We are happy to welcome you to our family.
    Pero si cortas HTML arbitrariamente en líneas, rompes las etiquetas

    • Era solo un ejemplo; perdón por no poder dar un ejemplo real con el número exacto de caracteres
    • La mayoría de las personas que reciben un correo HTML roto notarán que el formato del correo se ve raro, bajarán su evaluación de la competencia de la empresa por no poder escribir bien ni siquiera un correo legible, y pasarán al siguiente correo
      Es muy probable que la empresa ni se entere de ese pequeño problema
    • Si usas saltos de línea suaves de la codificación quoted-printable, puedes dividir líneas arbitrariamente sin romper etiquetas HTML
      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

    • Me da curiosidad cuál es el problema con 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