1 điểm bởi GN⁺ 2024-05-22 | 2 bình luận | Chia sẻ qua WhatsApp
  • Trong một hệ thống tạo email, tin nhắn SMS và PDF từ các mẫu tài liệu, đã xảy ra sự cố chỉ phần nội dung email gửi cho một khách hàng cụ thể bị mất dấu chấm
  • Mẫu gốc, bản xem trước và PDF đều có dấu chấm, nhưng sau khi thay giá trị placeholder theo từng khách hàng bằng dữ liệu thực, lỗi chỉ tái hiện ở một độ dài dòng nhất định
  • Client SMTP tùy chỉnh giới hạn độ dài dòng, đẩy dấu chấm thành ký tự đầu tiên của dòng mới, và do quy tắc dot transparency của SMTP, máy chủ đã loại bỏ dấu chấm đó
  • Cách sửa là để client thêm một dấu chấm nữa vào trước những dòng bắt đầu bằng dấu chấm và theo sau còn có ký tự khác
  • Một nhóm khác dùng chung mã client SMTP nhưng không vá lỗi, khiến email gửi tới một số khách hàng hiển thị phí bảo hiểm hằng tháng là $2700 thay vì $27.00; lỗi đã được sửa ngay lập tức

Vấn đề bắt đầu từ hệ thống mẫu tập trung

  • Khoảng 7 năm trước, một khách hàng muốn hợp nhất các tài liệu đang nằm rải rác trong nhiều mẫu Microsoft Word vào một hệ thống duy nhất
  • Cách làm cũ là nhân viên tự thay các placeholder trong tài liệu bằng tên, họ, v.v.; việc quản lý trở nên khó khăn khi các mẫu chứa điều khoản cũ, logo công ty cũ và phông chữ sai cùng được lưu hành
  • Hệ thống mới được cấu hình để quản lý tập trung các mẫu tài liệu, rồi dựa trên đó tạo tài liệu PDF, tin nhắn SMS và nội dung email
  • Thông điệp chào mừng khách hàng mới cũng có thể có các mẫu khác nhau tùy theo phương thức gửi
    • Phiên bản email có thể dùng bảng HTML và một số kiểu dáng cơ bản
    • Phiên bản gửi qua bưu điện có thể bao gồm infographic
    • Phiên bản tin nhắn SMS chỉ có thể chứa một lời chào mừng ngắn

Dấu chấm chỉ biến mất với một khách hàng cụ thể

  • Sau khi hệ thống đã vận hành vài tháng, hoặc hơn một năm, một quản trị viên báo rằng một dấu chấm bị thiếu trong nội dung email gửi cho một khách hàng cụ thể
  • Khi gửi cùng email đó cho khách hàng khác, dấu chấm không biến mất, nên khó coi đây chỉ là lỗi mẫu đơn giản
  • Trong mã nguồn của mẫu, dấu chấm đó thực sự tồn tại
  • Khi sao chép mẫu production sang môi trường local và tạo bản xem trước nội dung email, dấu chấm vẫn hiển thị; phiên bản để in được tạo từ cùng mẫu cũng hiển thị bình thường
  • Môi trường local gửi email tới localhost trên một cổng cụ thể, rồi kiểm tra email bằng một máy chủ SMTP giả như SMTP4dev và Outlook
  • Khi kiểm tra email gửi từ local lần đầu bằng Outlook, dấu chấm cũng hiển thị bình thường

Điều kiện tái hiện do dữ liệu theo từng khách hàng tạo ra

  • Khi tạo email, PDF và tin nhắn SMS, mẫu thay các giá trị placeholder như tên và họ của khách hàng bằng dữ liệu thực
  • Ngay cả với cùng một mẫu, nội dung email gửi cho từng khách hàng có thể có độ dài khác nhau
  • Khi tìm các giá trị placeholder thực tế đã dùng cho khách hàng gặp lỗi và gửi lại email trong môi trường local với đúng các giá trị đó, hiện tượng dấu chấm biến mất trong Outlook đã được xác nhận
  • Vấn đề phụ thuộc vào độ dài và nội dung cụ thể của email khách hàng đó
  • Ban đầu, họ kiểm tra xem ký tự dấu chấm trong mẫu có bị mã hóa không, hay có phải là một ký tự khác chứ không phải dấu chấm thật không, nhưng đó không phải nguyên nhân
  • Khi dịch vị trí dấu chấm trong mẫu đi một ký tự, dấu chấm lại hiển thị trong Outlook, và vị trí dòng trở thành manh mối để tái hiện lỗi

Độ dài dòng SMTP và dot transparency

  • Kết quả debug cho thấy mã lưu email vào cơ sở dữ liệu không thay đổi mẫu, ngoài việc thay placeholder bằng thông tin khách hàng
  • Sau đó, phạm vi điều tra được thu hẹp vào mã của cron job định kỳ lấy danh sách đối tượng gửi và gửi email
  • Một phần mã mà cron job gọi được lấy từ một dự án trước đó, trong đó có triển khai client SMTP tùy chỉnh
  • Mã này có một hàm ngắt dòng để mỗi dòng trong nội dung email không vượt quá một số ký tự nhất định
  • Hành vi này liên quan đến giới hạn độ dài dòng trong đặc tả SMTP
    • Tổng độ dài tối đa của một dòng văn bản là 1000 octets, bao gồm <CRLF>
    • Dấu chấm đứng đầu được nhân đôi để đảm bảo tính trong suốt không được tính vào con số này
    • Con số này có thể được tăng lên bằng SMTP Service Extensions
  • Trong nội dung email gặp lỗi, do giới hạn độ dài dòng, dấu chấm bị chuyển sang làm ký tự đầu tiên của dòng tiếp theo
  • Trong đặc tả SMTP, dấu kết thúc mail data được xử lý là một dòng chỉ chứa duy nhất một dấu chấm
  • Quy tắc dot transparency của SMTP xử lý riêng các dòng nội dung bắt đầu bằng dấu chấm
    • Trước khi gửi một dòng văn bản mail, client SMTP phải kiểm tra ký tự đầu tiên có phải là dấu chấm hay không; nếu là dấu chấm, phải chèn thêm một dấu chấm vào đầu dòng
    • Nếu một dòng chỉ có duy nhất một dấu chấm, máy chủ SMTP xử lý đó là kết thúc dữ liệu mail
    • Nếu ký tự đầu tiên là dấu chấm và trên cùng dòng còn có ký tự khác, máy chủ sẽ xóa ký tự đầu tiên
  • Client SMTP tùy chỉnh đã không thực hiện xử lý thêm một dấu chấm nữa khi dòng bắt đầu bằng dấu chấm, nên máy chủ SMTP nhận xóa dấu chấm đầu tiên, khiến dấu chấm thực sự trong nội dung biến mất

Cách sửa và tác động được phát hiện muộn

  • Cách sửa là để client thêm một dấu chấm nữa khi dòng bắt đầu bằng dấu chấm và trên cùng dòng còn có ký tự khác
  • Khi gửi như vậy, dù máy chủ SMTP nhận xóa dấu chấm đầu tiên, nội dung vẫn còn lại dấu chấm ban đầu
  • Khi gửi lại email gốc trong local với cùng thông tin người nhận của khách hàng gặp lỗi, dấu chấm không còn biến mất nữa
  • Sau khi bản sửa được triển khai, vì cùng mã client SMTP đã được lấy từ dự án trước đó để dùng, nhóm khác cũng được thông báo về lỗi này
  • Vài tháng sau, hệ thống của nhóm khác vẫn chưa được vá và đã gửi nhiều email quan trọng thông báo phí bảo hiểm hằng tháng mới cho khách hàng
  • Trong một số email, dấu chấm phân tách phần thập phân của phí bảo hiểm hằng tháng rơi đúng vào vị trí ký tự đầu tiên của dòng và biến mất
  • Kết quả là một số khách hàng nhận được email trông như phí bảo hiểm mới là $2700 chứ không phải $27.00
  • Lỗi này phụ thuộc vào độ dài từng dòng trong nội dung email và chỉ xảy ra với một số khách hàng có độ dài tên và họ khớp chính xác
  • Vì nguyên nhân đã được xác định từ trước, mã của nhóm đó đã được vá ngay lập tức

2 bình luận

 
surfindia 2024-05-22

Có vẻ như trong tiêu đề, period đã được hiểu là khoảng thời gian chứ không phải dấu chấm haha

 
GN⁺ 2024-05-22
Các ý kiến trên Hacker News
  • Một phần đoạn mã này đang tự triển khai SMTP client, và nguyên nhân gốc có vẻ nằm ở đó
    Triển khai giao thức cho đúng là việc khó, và những lỗi như trong bài viết là chuyện phổ biến
    Nếu là một thư viện SMTP client được triển khai đúng, nó sẽ mã hóa theo chuẩn SMTP bất kể vị trí dấu chấm trong văn bản đầu vào, và tầng template sẽ không cần bận tâm đến SMTP

    • Vấn đề thật sự không nằm ở bản thân giao thức, mà ở cách tiếp cận kiểu cao bồi khi xử lý giao thức đó
      Nhận văn bản rồi mã hóa theo chuẩn SMTP không khó, nhưng trước hết phải biết rằng cần có bước xử lý đó
      Vô số lỗi và lỗ hổng bảo mật như SQL injection, XSS cũng sinh ra từ cùng một sai lầm, tức là nối chuỗi
      Với truy vấn SQL, việc bind giá trị vào template truy vấn phải diễn ra trong “không gian SQL”, chứ không phải trong không gian chuỗi không kiểu; tự tạo chuỗi SQL đã tuần tự hóa như SELECT * FROM foo WHERE foo.bar = + $userData là cách làm sai
      Template HTML cũng vậy: nếu xử lý ở mức cây tài liệu thay vì biểu diễn chuỗi, ta có thể tránh các lỗ hổng ngớ ngẩn
      Để tránh việc dấu chấm biến mất trong email, đừng chèn văn bản không có cấu trúc vào giữa pipeline SMTP; hãy tôn trọng mức trừu tượng mà mình đang làm việc
      Liên quan đến chuyện này, langsec cũng đáng xem
    • Vấn đề thật sự là SMTP là một giao thức “văn bản thuần” có chứa tín hiệu in-band
      Vì “một dòng chỉ có một dấu chấm” được định nghĩa là chuỗi điều khiển chứ không phải một dòng ký tự theo nghĩa đen, nên chuyện này mới xảy ra
      SMTP là ví dụ về một thiết kế phức tạp không cần thiết, và lỗi triển khai cũng phản ánh chính sự phức tạp đó
      Không phải là nên khuyên mọi người tự triển khai, nhưng SMTP lẽ ra không nên khó đến mức một người tự triển khai cũng khó làm đúng
    • Nghe thì có vẻ là ý hay, nhưng bạn kéo về một thư viện SMTP, rồi thư viện đó kéo theo 5 dependency, mỗi cái lại kéo thêm 3 dependency bắc cầu, và một trong số đó có thể là một dự án hộp công cụ vạn năng mà thực tế bạn chỉ dùng 1%
      Phần còn lại là gánh nặng vô ích, nhưng vì những hàm thậm chí không bao giờ được gọi, nó vẫn kéo thêm 20 dependency nữa; chẳng mấy chốc codebase phình ra thêm vài MB, và bạn bắt đầu nhận cảnh báo CVE từ một thư viện mà bạn còn không biết mình đang dùng
    • https://en.wikipedia.org/wiki/Jamie_Zawinski#Zawinski's_Law
  • Phần kỹ thuật thì những người khác đã nói hay hơn rồi, nhưng chuyện này gợi tôi nhớ đến một giai thoại về việc một thứ nhỏ nhặt như một dấu chấm ở cuối câu có thể quan trọng đến mức nào
    Ở Đức, nơi tôi làm việc, khi nghỉ việc người ta thường yêu cầu một “Zeugnis”, tức thư giới thiệu ghi các nhiệm vụ đã làm và đánh giá nhân viên; đây thường là tài liệu quan trọng khi tìm việc
    Tất nhiên nhân viên sẽ không chấp nhận những câu kiểu “người này lười, đừng tuyển”, nên đã hình thành một dạng mật mã gọi là “Zeugnissprache”, được ngụy trang như lời khen
    Một trong các mật mã đó là nếu câu cuối không có dấu chấm, nghĩa là “hãy bỏ qua mọi thứ viết ở đây, người này rất tệ”
    Sau công việc trước, tôi đã nhờ luật sư xem lại Zeugnis của mình; dù mọi đánh giá đều tích cực, có lẽ do bất cẩn mà câu cuối lại thiếu dấu chấm

    • Chuyện có mật mã bí mật trong thư giới thiệu là truyền thuyết đô thị
      Nhân sự không có động cơ để tạo ra một mật mã bí mật dùng chung với các đối thủ tiềm năng, và việc vừa dạy nó cho nhân sự mới vừa giữ bí mật cũng chẳng hợp lý
      Những truyền thuyết như vậy xuất hiện vì bộ phận nhân sự không thể viết quá thẳng thừng rằng ai đó từng là một rắc rối
      Nếu không có gì tích cực để viết, họ sẽ khen những ưu điểm tầm thường như đúng giờ; đó giống kiểu nói nhân sự tương đương với “bless their heart” hơn là mật mã bí mật
      Nếu bị kiện, họ vẫn còn đường lui kiểu “Thưa tòa, đó là lời tốt đẹp mà! Người này lúc nào cũng đến đúng giờ!”
    • “Nếu câu cuối không có dấu chấm thì hãy bỏ qua mọi thứ viết ở đây, người này rất tệ” — nghe là biết có thể có chuyện gì sai được cơ chứ
    • Không biết bạn đã kiểm tra xem trong tài liệu đánh giá hiệu suất cũng có bị thiếu dấu chấm không
  • Tôi không hiểu vì sao một cron job gửi email lại phải tự triển khai SMTP client
    Cứ dùng một chương trình như mail của mailutils là được
    Xét từ góc độ khả năng gửi đến nơi, tự dựng một tương tác SMTP sơ sài trên socket ngay từ đầu đã là quá sức
    Ngày nay bạn không thể trực tiếp kết nối đến bất kỳ mail exchanger host nào để gửi thư; thường phải kết nối đến một SMTP relay host cụ thể do ISP cung cấp
    Muốn vậy thì phải triển khai cả kết nối TLS, xác thực, v.v.
    Hơi mỉa mai là bản thân cron vốn đã biết gửi mail
    Output của cron job sẽ được gửi mail cho chủ sở hữu, và một số cron cho phép đổi địa chỉ nhận bằng những thứ như biến MAILTO trong crontab

    • Dù đúng ra khi gửi mail nên dùng MTA của host, trên thực tế có rất nhiều thứ nhúng sẵn SMTP client riêng
      Lý do là người dùng muốn dùng “thứ đó” có thể nhập địa chỉ SMTP server, nhưng chương trình không có cách nào tin rằng một MTA gửi như sendmail đã được cấu hình đúng
      Trong các công ty tồn tại rất nhiều server không thể gửi mail hệ thống, và tình trạng đó nhiều khi nằm ngoài quyền kiểm soát của cả người viết chương trình lẫn người dùng
      Cấu hình mail là một lĩnh vực chuyên môn cần các điều kiện tiên quyết như DNS, mã hóa, chính sách; chỉ để một thứ như ứng dụng ví gửi được mail thì như vậy là quá mức
      “Hãy hỏi quản trị viên hệ thống” thường không phải là lựa chọn thực tế, dù ở quy mô lớn hay nhỏ
      Đó không phải là lý do hay, nhưng là lý do thực tế
    • Không có thế giới nào mà ta có thể tin chắc hệ thống đã có sẵn một binary để gửi mail, nhưng dù vậy tôi cũng sẽ không tự viết
      Với hầu hết ngôn ngữ, dù không có trong thư viện chuẩn thì cũng có rất nhiều thư viện SMTP client dùng được
    • Tôi thấy chuyện này cũng là một ví dụ của định luật Zawinski
  • Ở đây thấy có hai thói quen xấu lớn
    Thứ nhất, như nhiều người đã chỉ ra, là đừng triển khai tiêu chuẩn một cách qua loa
    Nếu phải tự triển khai, hãy dành sự chú ý và công sức cần thiết, hoặc dùng một thư viện đã có sẵn
    Thứ hai là đừng vendor các dependency
    Các thư viện đang dùng cần được cập nhật định kỳ và đúng lúc, chứ không phải “chỉ khi cần”
    Nếu trì hoãn hoặc né tránh cập nhật, ngay cả các lỗi đã được upstream sửa rồi cũng có thể trở thành vấn đề lớn với những người nghĩ rằng chỉ nên cập nhật khi chính mình thấy có vấn đề

    • Phương án thay thế cho việc không vendor dependency trông còn tệ hơn
      Khi đó độ ổn định của ứng dụng bị phơi ra trước các thay đổi từ upstream, và những thay đổi đó có thể làm hỏng code
      Có thể không nhận được bản sửa ngay lập tức, nhưng tôi nghĩ thà biết rằng mình thay đổi vì một bản sửa mình cần còn hơn là đưa vào sự bất ổn không mong muốn và rủi ro bổ sung
      Tôi thuộc phe “chưa hỏng thì đừng sửa”
  • Dạo này có vẻ nhiều người không học giao thức cơ bản bằng cách tương tác trực tiếp qua terminal
    SMTP vốn có vẻ được thiết kế với kiểu tương tác thủ công như vậy, và với tư cách là người từng dùng như thế một thời gian, “một dòng chỉ có một dấu chấm” để kết thúc thông điệp đã khắc sâu vĩnh viễn trong trí nhớ
    Liên quan đến đó, việc escape dường như cũng là một khái niệm xa lạ với nhiều lập trình viên
    Nhìn tình huống trên, có nhiều người không hỏi “nếu muốn gửi một email có một dòng chỉ chứa một dấu chấm thì sao?”, nhưng cũng có một nhóm lớn khác sẽ thấy câu hỏi đó rất logic và dễ hiểu

    • Nếu trong 30 năm qua có một tỷ lệ đáng kể nhà phát triển phần mềm học theo cách đó thì tôi mới thấy ngạc nhiên
  • Cần dot stuffing
    SMTP https://www.rfc-editor.org/rfc/rfc5321#section-4.5.2
    POP3 cũng có https://www.rfc-editor.org/rfc/rfc1939#page-8

  • Nhớ lại trải nghiệm debug một implementation giao thức mạng. Cụ thể là AppleTalk NBP cho những người thuộc thế hệ cũ
    Tôi đã code xong hết, nhưng packet của tôi bị từ chối, tức là bị drop âm thầm, còn packet từ implementation Apple thật thì đi qua
    Tôi đặt packet tốt và packet xấu lên màn hình máy tính rồi so sánh từng byte, nhưng không tìm ra vấn đề; từ đầu đến cuối chúng giống hệt nhau, kể cả checksum
    Đến giờ tan làm, tôi quyết định in mấy thứ ngớ ngẩn đó ra để xem sau, và ngay khi in ra thì thấy lỗi
    Phiên bản của tôi dài hai trang, còn implementation đúng chỉ một trang
    Hóa ra tôi đã không xóa buffer đúng cách trước khi gửi dữ liệu. mbuf là thế đấy
    Nghĩ lại vẫn buồn cười

  • Dòng “We are happy to welcome you to our family.” còn lâu mới gần tới giới hạn độ dài dòng
    Có vẻ đã có chuyện gì khác, chẳng hạn toàn bộ nội dung thực ra là một attachment HTML MIME
    Có thể là kiểu như sau: ... lots of text ... rồi đến We are happy to welcome you to our family.
    Nhưng nếu cứ cắt HTML thành dòng một cách tùy tiện thì sẽ làm hỏng tag

    • Đây chỉ là ví dụ thôi, xin lỗi vì không đưa ra được ví dụ thực tế có đúng số ký tự
    • Phần lớn người nhận email HTML bị hỏng sẽ nhận ra định dạng email có vấn đề, xem đó là một công ty đến email dễ đọc cũng không viết nổi nên hạ thấp đánh giá năng lực công ty, rồi chuyển sang email tiếp theo
      Công ty rất có thể sẽ hoàn toàn không hay biết vấn đề nhỏ đó
    • Nếu dùng xuống dòng mềm của mã hóa quoted-printable, có thể chia dòng tùy ý mà không làm hỏng tag HTML
      quoted-printable đáng ra phải có giới hạn tối đa 78 ký tự, bao gồm CRLF, nhưng các email client nhìn chung khá dễ dãi
  • Nếu đây là lần đầu nghe đến dot stuffing, bạn sẽ khó tin nổi trong thế giới email còn có những nỗi kinh hoàng nào khác
    Có những thứ như gập header, xử lý dấu ngoặc kép trong local part, và IPv6 literal

    • Không rõ IPv6 literal thì có vấn đề gì
  • Ngay khi đọc đến đoạn nói rằng đó là code SMTP client mượn từ một dự án trước, tôi đã đoán kết cục sẽ là đội khác vẫn chưa patch bug này