Vụ kỳ lạ về dấu chấm biến mất
(tjaart.substack.com)- 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
- Tổng độ dài tối đa của một dòng văn bản là 1000 octets, bao gồm
- 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à
$2700chứ 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
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 hahaCá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
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 =+$userDatalà cách làm saiTemplate 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ì “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
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
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
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ờ!”
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ư
mailcủa mailutils là đượcXé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
MAILTOtrong crontabLý 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ế
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
Ở đâ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 đề
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
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 đếnWe 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
Công ty rất có thể sẽ hoàn toàn không hay biết vấn đề nhỏ đó
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
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