Tuần trước mình pin một plugin Claude Code theo đúng commit SHA, kiểu mình vẫn luôn làm khi không tin tưởng hoàn toàn một repo bên thứ ba. 40 ký tự hex, bất biến, tưởng như không thể giả mạo. Ba nhà nghiên cứu ở AIR Security — Or Nevo, Dor Granat, Niv Hoffman — vừa công bố một paper chứng minh cái giả định đó sai với bốn agent khác nhau, và cách xử lý sau đó cũng không giống một CVE bình thường chút nào.

Lỗ hổng tên là Plugin4Shell. Nhìn ra cơ chế rồi thì thấy nó đơn giản tới mức hơi… khó chịu.

Một dòng hành vi của Git mà chẳng ai kiểm tra lại

Claude Code, OpenAI Codex, GitHub Copilot, Gemini CLI — cả bốn đều khóa plugin theo một commit hash, rồi chạy thứ tương đương git checkout <SHA> trước khi thực thi. Toàn bộ mô hình an toàn nằm gọn ở đó: pin hash, nhận đúng code đó, hết chuyện.

Trừ việc Git không resolve ref theo đúng cái mental model đó. Đưa cho Git một chuỗi 40 ký tự, nó không đi thẳng vào object database. Nó check branch và tag trước. Nếu tình cờ có một branch mang tên đúng 40 ký tự hex mà bạn đã pin, Git sẽ checkout cái branch đó. Không phải commit.

Vậy nên kẻ tấn công kiểm soát repo plugin chỉ cần tạo một branch mang tên đúng SHA bạn đã khóa, trỏ branch đó tới bất cứ code độc hại nào họ muốn, và mọi agent đã “pin” plugin của bạn sẽ chạy phiên bản của họ thay vì của bạn. Gemini CLI có biến thể riêng của cùng lỗi này: nó tin vào FETCH_HEAD, thứ kẻ tấn công cũng dễ dàng trỏ lại y hệt. Không jailbreak. Không prompt injection. Không social engineering. Bạn làm đúng mọi thứ một kỹ sư cẩn thận được dạy phải làm, và chính cơ chế an toàn lại là lời nói dối.

Mình đã mất mười lăm năm nói với các bạn junior rằng pin theo SHA là cách làm có trách nhiệm, chấm hết, không bàn cãi. Đây là lần đầu tiên mình phải thêm một dấu hoa thị vào câu đó.

Bốn vendor, bốn phản ứng khác nhau

Điều khiến mình chú ý không phải là bug — bug thì lúc nào chả có — mà là mỗi vendor xử lý ra sao sau khi AIR Security báo cáo.

Claude Code ra bản vá ở 2.1.179. Codex vá ở 0.146.0. Cả hai đóng lỗ hổng ref-ambiguity theo cùng một cách: resolve thẳng ra object, bỏ qua bất cứ ref nào trùng chuỗi ký tự.

GitHub Copilot tới lúc mình viết bài này vẫn chưa vá.

Google không vá Gemini CLI. Họ deprecate luôn.

Mình cứ quay lại nghĩ về cái quyết định cuối cùng đó. Deprecate một sản phẩm là một phản ứng hợp lý trước một lỗ hổng — đôi khi cách xử lý trung thực nhất đúng là “chúng tôi không maintain cái này nữa” — nhưng nó cũng có nghĩa là mọi kỹ sư từng chọn Gemini CLI vì tin vào lời hứa pin-plugin an toàn, giờ đang cầm trong tay một công cụ có lỗ hổng supply-chain đã biết và sẽ không bao giờ được vá, trừ khi họ đã migrate đi rồi. Nếu team bạn vẫn đang chạy nó, việc migrate giờ không còn là chuyện tùy chọn nữa.

Bán kính ảnh hưởng cũng không đồng đều. Marketplace plugin của chính GitHub chặn tên branch giống hash, nên install từ github.com không bị lộ qua vector này. Bitbucket và Git self-hosted thì không có rào chắn đó. Nếu org của bạn chạy plugin registry nội bộ trên hạ tầng self-hosted — mà kha khá team enterprise mình từng tư vấn làm đúng vậy — bạn đã thừa hưởng rủi ro này ngay từ lúc ai đó bật auto-update.

Chính auto-update là thứ biến một bug lý thuyết thành zero-click thật sự. Claude Code và Codex đều auto-update plugin theo mặc định. Bạn không cần chạy lệnh install để branch độc hại được kéo về — agent tự fetch theo lịch, rồi báo lại rằng phiên bản “đã khóa” đang được cài, vì theo đúng logic của chính nó, đúng là như vậy thật.

Thứ mình thực sự đổi trong tuần này

Mình tắt auto-update plugin trên mọi instance Claude Code mình đang chạy. Mình check lại các repo plugin nội bộ xem có hoạt động branch bất thường không — không có gì, âu cũng là một tin tốt. Và mình thêm đúng một dòng vào checklist vet plugin: verify cái pin resolve ra đúng một commit object, chứ không chỉ là có một chuỗi giống SHA nằm trong config.

Cái check đó rẻ tới mức hơi buồn cười — git cat-file -t <SHA> trước khi tin bất cứ thứ gì, năm giây, xong. Lý do nó chưa từng nằm trong checklist là vì “pinned theo SHA” đã đóng vai trò như một cách nói tắt cho “an toàn” trong mọi buổi code review mình từng ngồi suốt cả chục năm nay. Plugin4Shell là lời nhắc rằng cách nói tắt đó đang gánh một lời hứa mà cơ chế bên dưới chưa bao giờ thật sự đưa ra.

Đây không hẳn là một câu chuyện về Git. Tooling cho agent đang thừa hưởng cả chục năm giả định về ý nghĩa của việc “pin một dependency”, xây trên hạ tầng — package manager, cách VCS resolve ref — chưa bao giờ được thiết kế cho một consumer tự hành, tự auto-update ngồi phía trên nó. Mọi giả định kiểu đó đều đáng để audit lại trước khi bạn để một agent chạm vào nó, và mình nghi Plugin4Shell không phải lỗ hổng cuối cùng mình sẽ tìm ra theo cách này.

Xuất nội dung

Bình luận