# Cài thư viện là một lần chạy code lạ: cái cổng nhập cảnh mà hệ tự động hoá nào cũng bỏ ngỏ
Có một câu mình đã tin suốt nhiều năm mà không hề kiểm lại: cài một thư viện là tải một thư viện về.
Nó sai. Cài là chạy. Và cái mình chạy không phải thứ mình đọc.
Mình nhận ra điều đó không phải nhờ một buổi học bảo mật, mà nhờ một buổi sáng hệ vẫn chạy bình thường. Nội dung vẫn ra, việc vẫn xong, không có cái log nào đỏ. Chỉ có một chi tiết nhỏ: trong lúc mình ngủ, có thứ thứ hai cũng đã chạy trong cùng cái máy đó, và nó không xin phép.
Tuần này mình đọc một bài phân tích công khai về đúng kiểu chuyện ấy, xảy ra với một thư viện gateway LLM rất phổ biến — loại thư viện mà hàng nghìn hệ tự động hoá đang cắm ở giữa để gọi model. Một bản release trên kho package công khai bị chèn thêm file lạ. Bản đó vẫn qua được quy trình build, vẫn cài sạch, vẫn chạy đúng như tài liệu. Nó làm thêm ba việc nữa: hốt khoá nằm trong môi trường, gửi về một domain trông rất giống thật, và dựng một tiến trình nhỏ thức dậy theo giờ để đọc lệnh từ bên ngoài.
Không có triệu chứng nào ở tầng người dùng. Chỉ có một hệ vẫn xanh, và một đường ống dẫn dữ liệu ra ngoài cũng xanh theo.
Mình ngồi lại với cái tin đó khá lâu, vì nó chỉ đúng vào chỗ mình yếu nhất.
"Cài" không phải "tải" — và mình đã bỏ qua chữ đó bao nhiêu năm
Cơ chế đáng sợ nhất trong vụ này không phải là mã độc, mà là chỗ nó núp: một file cấu hình đường dẫn trong thư mục thư viện đã cài.
Nghe thì vô hại. Nhưng trình thông dịch đọc loại file đó mỗi lần khởi động, và thực thi nội dung mà nó trỏ tới. Nghĩa là không cần ai gõ import gì cả. Không cần chương trình của mình gọi tới thư viện đó. Chỉ cần có một tiến trình chạy lên — bất kỳ script nhỏ nào, bất kỳ lịch tự chạy nào, bất kỳ lần mình mở terminal kiểm tra gì đó.
Minh hoạ ở mức khái niệm (pseudo-code, không phải nguồn triển khai):
# trong thư mục thư viện đã cài
# một file chỉ chứa đường dẫn tới nội dung sẽ được nạp lúc khởi động
/path/to/payload
# mỗi lần trình thông dịch bật lên:
# -> đọc file này
# -> nạp nội dung được trỏ tới
# -> chạy nội dung đó với đúng quyền của người đang chạyĐiểm mấu chốt nằm ở dòng cuối: với đúng quyền của người đang chạy. Không phải quyền của thư viện đó, không phải một hộp cát riêng. Quyền của mình.
Và đây là điều làm mình lạnh người: cái file đó không có gì để "đọc hiểu". Nó không có logic nghiệp vụ sai, không có hàm nào trả về kết quả lạ, không có chỗ nào để review. Một dòng. Và một dòng thì review xong trong bao lâu? Đúng bằng thời gian mắt lướt qua nó.
Cách này không phải độc quyền của một ngôn ngữ. Mỗi hệ sinh thái đều có một cửa tương tự: script chạy sau khi cài, bước build tùy biến, entrypoint của container, extension của trình soạn thảo, plugin của công cụ tự động hoá. Mình gọi tất cả những thứ đó là "cấu hình", nhưng về mặt thực thi, chúng đều là một lần chạy code lạ có quyền bằng mình.
Giờ hãy đếm: cái hệ tự động hoá của mình, cái đang tự kiếm việc, tự đăng bài, tự gọi model, tự ghi file — nó được ghép từ bao nhiêu lần "chạy code lạ" như thế? Mình đếm ra hàng trăm. Mình đã tin hàng trăm lần, và mình chỉ thật sự đọc được vài lần.
Nghịch lý: càng gọn hệ, càng dồn khoá vào một chỗ
Vụ này nhắm vào một thư viện gateway — thứ nằm giữa ứng dụng và nhà cung cấp model. Và mình nghĩ đó không phải ngẫu nhiên. Đó là chỗ đắt nhất trong một hệ tự động hoá hiện đại.
Hãy nhìn cái gateway trong hệ của mình. Nó giữ gì?
Khoá gọi model. Khoá của vài công cụ bên thứ ba. Thông tin truy cập hạ tầng. Token của đường ống triển khai. Và thường là cả một bảng trong database ghi khoá của từng khách hoặc từng dự án con, vì mình muốn đổi model ở một chỗ thay vì sửa mười chỗ.
Mình gom khoá lại vì lý do rất chính đáng: dễ đổi, dễ đo chi phí, dễ giới hạn tốc độ. Nhưng cái mình vừa làm, về mặt cấu trúc, là tự xây một cái kho. Và cái kho thì luôn hấp dẫn hơn cái tủ nhỏ rải rác.
Đây là chỗ mình thấy nhiều anh em (kể cả mình) hiểu sai về rủi ro. Mình hay nghĩ: "hệ của mình bé, không ai thèm tấn công". Nhưng kẻ tấn công không nhắm hệ của mình. Nó nhắm một dependency mà mình tin, vì một dependency được tin bởi mười nghìn người thì đầu tư một lần, thu về mười nghìn cái kho.
Mình không cần bị hack. Mình chỉ cần tin đúng chỗ mà người khác đã chọn để phá.
Ba thứ khiến mình sẽ không thấy gì cả
Đây là phần làm mình khó chịu nhất, vì nó không phải về kỹ thuật mà về khả năng nhận biết.
Một, log vẫn xanh — vì hệ đang chạy đúng thứ nó được cấp. Không có exception, không có timeout, không có dòng "lỗi xác thực". Mã độc chạy tốt nhất là mã độc không làm hỏng việc gì. Tín hiệu duy nhất có thể có là một kết nối ra ngoài tới chỗ lạ — và nếu mình không có danh sách "được phép đi đâu", thì mọi đường ra đều trông như đường ra bình thường.
Hai, chữ ký không cứu được. Người ta hay nói "cứ dùng bản có chữ ký/checksum là an toàn". Nhưng chữ ký chỉ trả lời một câu: ai đã đẩy bản này. Nó không trả lời bản này chứa cái gì. Nếu tài khoản đẩy bản phát hành bị chiếm, hoặc bước build bị chèn, thì bản độc vẫn có chữ ký hợp lệ. Chữ ký bảo vệ đường truyền, không bảo vệ nội dung.
Ba, "bản mới nhất" là một quyết định mình đã giao cho người khác. Mặc định của hầu hết công cụ là lấy bản mới nhất. Nghĩa là mỗi lần cài lại, mình tự động nhận một thứ mà chưa ai kịp soi — thường là trong khoảng vài giờ đầu sau khi phát hành, đúng cái cửa sổ mà kẻ tấn công cần. Mình không chọn rủi ro đó. Mình chỉ không chọn gì cả, và mặc định chọn hộ.
Cộng ba thứ lại: hệ vẫn chạy, chữ ký vẫn đúng, bản vẫn "mới". Không có cái nào trong ba cái đó là tín hiệu xấu. Đó là lý do một vụ như tuần này có thể sống nhiều giờ, nhiều ngày, mà người dùng cuối không biết gì.
Tầng nhập cảnh: sáu việc làm trong một buổi chiều
Mình không có đội bảo mật. Mình cũng không nghĩ bài này nên khuyên ai dựng một hệ thống giám sát hoành tráng. Cái mình làm sau khi đọc tin đó là rà lại cái cổng — thứ gì được phép vào hệ, và vào bằng cách nào. Sáu việc, cái nào cũng làm được một mình.
1. Pin cứng, và có file khoá phiên bản. "Mới nhất" phải là một hành động có chủ ý, không phải mặc định. Mình chuyển sang ghi cứng phiên bản đã kiểm, và giữ một file khoá kèm mã băm để lần cài sau không thể lặng lẽ khác lần trước. Câu hỏi tự soi rất đơn giản: máy này, hôm qua và hôm nay, có đúng là cùng một bộ thư viện không? Nếu không trả lời được trong ba mươi giây, mình không kiểm soát được cổng.
2. Tách chỗ cài khỏi chỗ chạy. Đây là việc hiệu quả nhất mà lại ít người làm. Giai đoạn cài đặt không được nhìn thấy khoá nào. Giai đoạn chạy không được cài gì cả. Nếu bước cài chạy trong một môi trường không có credential, thì kể cả bị chèn mã, thứ bị lấy cũng chỉ là... một cái máy trống.
Pseudo-code ở mức ý tưởng (không phải nguồn triển khai):
# giai đoạn build: không có secret nào trong môi trường
build:
cài dependency theo bản đã pin + khoá phiên bản
chạy kiểm thử
đóng gói
# giai đoạn chạy: đã có secret, nhưng không được cài thêm gì
run:
nạp secret từ nơi giữ riêng
khởi động hệ
# nếu có ai cố cài thêm package ở đây -> đó là tín hiệu, không phải tính năng3. Khoá không nằm trong tầm với của process sinh nội dung. Cái process gọi model để viết, để tóm tắt, để trả lời — đó là process chạm vào nhiều thứ lạ nhất (nội dung web, dữ liệu khách, đầu ra của model khác). Nó không nên cầm chìa. Nó nên cầm một vé ngắn hạn, hết hạn nhanh, chỉ mở đúng một cửa. Gateway giữ chìa. Worker cầm vé. Nếu worker bị lừa, thứ mất là một cái vé đã hết hạn.
4. Đường ra phải có danh sách. Hệ của mình thật ra chỉ cần nói chuyện với vài ba chỗ. Nếu chỉ có vài ba chỗ, thì chỗ thứ tư là tín hiệu, không phải "chắc do mạng chập chờn". Mình không dựng được hệ chặn hoàn hảo, nhưng chỉ cần một dòng log "kết nối ra ngoài tới chỗ không nằm trong danh sách" là mình đã có thứ mà cả vụ tuần này thiếu: một cái log biết đỏ.
5. Khoá phải xoay được trong năm phút. Đây là bài kiểm tra mình thích nhất vì nó trả lời thẳng câu hỏi "mình có thật sự sở hữu khoá không". Nếu câu trả lời là "phải sửa mười hai file rồi deploy lại, chắc chiều xong", thì mình không sở hữu khoá. Mình đang giữ con tin. Và trong một sự cố thật, năm phút và nửa ngày là khác nhau đúng bằng khoảng thời gian dữ liệu còn đang chảy ra.
6. Mỗi tuần mười phút đối chiếu. Không phải audit lớn. Chỉ ba câu: tuần này có dependency nào mới vào hệ? Khoá nào còn sống mà mình không nhớ vì sao nó sống? Có process nào đang chạy mà mình không giải thích được nó sinh ra để làm gì? Mình đã bỏ câu hỏi thứ ba suốt một thời gian dài, và đó chính là chỗ một tiến trình nhỏ thức theo giờ có thể sống rất lâu.
Nối vào chuyện tiền: đây không phải rủi ro kỹ thuật, đây là điều khoản hợp đồng
Nếu mình chỉ tự động hoá cho mình, sự cố này là chuyện của mình. Nhưng nếu mình bán automation, nó thành chuyện khác hẳn.
Khách không sợ model trả lời ngu. Model trả lời ngu thì khách sửa tay, càu nhàu, rồi vẫn dùng. Cái khách không tha là dữ liệu của khách đi đâu đó mà không ai báo. Một vụ supply chain không phải "hệ bị lỗi", nó là "hệ của anh đã đọc dữ liệu của tôi rồi gửi cho người khác" — và câu đó không sửa được bằng một bản vá.
Nên mình thấy giá trị bán được không nằm ở chỗ "hệ của tôi không bao giờ bị chèn". Không ai hứa được điều đó. Giá trị nằm ở ba thứ đo được:
- ▹Mình biết trong bao lâu. (Có tín hiệu, có ngưỡng, có người nhận cảnh báo.)
- ▹Mình chứng minh được cái gì đã đi ra. (Chứ không phải "chắc là không sao".)
- ▹Mình xoay khoá và cắt đường trong bao lâu. (Cam kết bằng phút, không bằng ngày.)
Ba thứ đó chính là tầng bằng chứng mà mình vẫn nói ở các bài trước, chỉ là lần này nó áp lên cái cổng thay vì áp lên con bot. Và nó cũng là chỗ biên lợi nhuận thật của dịch vụ automation: khâu viết workflow giờ rẻ, ai cũng làm được trong một buổi chiều. Khâu giữ cho nó đáng tin trong hai năm thì không rẻ, và rất ít người bán nó tử tế.
Nói cách khác: mình không bán "một con bot chạy được". Mình bán "một con bot mà khi có chuyện, anh gọi được cho ai đó và nhận được câu trả lời trong mười phút".
Ba câu hỏi mình tự hỏi lại sau khi đọc tin đó
Nếu đêm nay một dependency của mình bị chèn mã độc, sáng mai mình biết bằng cách nào? Nếu câu trả lời là "chắc đọc tin", thì mình không có hệ, mình có may mắn.
Khoá của mình đang nằm trong tầm với của bao nhiêu process? Càng nhiều process cầm chìa, càng nhiều cửa để một dòng cấu hình vô hại bước vào.
Bản mình đang chạy hôm nay có giống bản mình đã kiểm tuần trước không? Nếu không trả lời được, thì toàn bộ công sức review của mình chỉ là review một thứ khác.
Mình không viết bài này để làm ai sợ. Mình viết vì cái làm mình sợ không phải mã độc — mà là sự im lặng. Một hệ chạy đúng, log xanh, mọi thứ bình thường, và vẫn có thứ thứ hai đang chạy cùng.
Nếu anh em cũng đang vận hành mấy chuỗi tự động hoá tự kiếm việc, mình tò mò muốn biết: cái cổng nhập cảnh của hệ anh em hiện có bao nhiêu cửa, và có cửa nào anh em chưa từng mở ra nhìn? Comment cho mình biết, mình đọc hết.
zeodavu.com