Thử tưởng tượng cái dòng log bạn thấy hàng ngày. CI job của bạn khởi động một agent, agent pull một plugin về, console in ra kiểu Verified commit a3f9c2e1... OK. Bốn mươi ký tự hex, dấu tick xanh, xong. Bạn đi làm việc khác. Cái dấu tick đó chính là toàn bộ mô hình tin cậy của một phần rất lớn hệ sinh thái AI coding agent hiện nay, và tính đến 18/9/2026, các nhà nghiên cứu ở Air Security đã chỉ ra rằng nó có thể vô giá trị.
Con bug này có cái tên nói lên hết: Plugin4Shell. Cùng dạng với Log4Shell — một đường load dependency mà chẳng ai soi kỹ cho đến khi nó bị khai thác thật — chỉ khác là lần này thứ load dependency là một agent tự động đang cầm SSH key của bạn.
Cái gì thật sự bị hỏng
Claude Code, OpenAI Codex, GitHub Copilot, và Google Gemini CLI đều hỗ trợ plugin pin theo một commit hash cụ thể. Ý tưởng đúng — pin theo SHA chứ không phải tên branch, để không ai lặng lẽ thay code dưới chân bạn được. Vấn đề nằm ở chuyện xảy ra sau khi khai báo pin đó.
Agent yêu cầu Git checkout commit a3f9c2e1.... Nó nhận lại một working tree. Nó báo cáo cái SHA mà nó đã yêu cầu. Cái mà cả bốn công cụ này không làm — cho đến khi bản vá ra đời — là verify xem cái tree thực sự checkout ra có khớp từng byte với hash đó không.
Với Claude Code, Codex và Copilot, đường khai thác đi qua cơ chế resolve reference của Git: kẻ tấn công tạo một branch có tên trùng đúng 40 ký tự commit hash mà agent đang tìm. Git có thể resolve cái tên branch đó trước khi resolve object commit thật trong lúc checkout. Agent nghĩ nó đã fetch a3f9c2e1. Thực ra nó fetch bất cứ thứ gì kẻ tấn công đặt trên một branch tình cờ có tên a3f9c2e1. Biến thể trên Gemini CLI còn trơ trẽn hơn — nó lợi dụng một branch trong repo có tên đúng là FETCH_HEAD, đánh lạc hướng checkout khỏi commit thực sự được fetch về.
Zero-click đúng nghĩa đen luôn. Không hộp thoại xác nhận, không câu hỏi “bạn có muốn cài plugin này không”, không bước cài lại nào để nạn nhân bấm đồng ý. Nếu agent của bạn bật auto-update cho plugin — mặc định ở hầu hết setup vì ai mà rảnh đi bump tay bốn chục cái version plugin — thì việc tráo đổi diễn ra âm thầm ngay lần refresh nền tiếp theo.
Và code độc hại thừa hưởng được gì khi chạy? Bất cứ thứ gì developer đang chạy agent có: quyền truy cập source code local, cloud credentials, SSH key, quyền vào internal repo, và ở nhiều công ty là cả đường đi thẳng vào production. Bài viết của Air Security nêu ra hai kịch bản tấn công thực tế. Một: publish một plugin trông có vẻ hợp pháp, để nó được nhiều người dùng theo thời gian, rồi sau đó lật kèo repo upstream. Hai — và cái này mới đáng lo hơn — bạn còn chẳng cần publish gì mới: “xâm nhập vào repo của một maintainer plugin đang có sẵn cũng cho ra kết quả tương tự.” Cái plugin bạn tin tưởng, đã cài từ lâu, chính là bề mặt tấn công. Bạn chẳng làm gì sai cả mà vẫn dính.
Ai vá, ai không, ai bó tay
Đây là chỗ bốn vendor phân hóa rõ rệt, và đáng nói tên ra vì phản ứng của họ thực sự không ngang nhau.
Anthropic (Claude Code) — đã vá ở phiên bản 2.1.179. Nếu bạn đang dùng bản cũ hơn, bạn đang bị lộ. Đây là phản ứng nhanh nhất trong bốn cái, và cách xử lý rõ ràng: cập nhật là xong.
OpenAI (Codex) — vá ở phiên bản 0.146.0. Cùng kịch bản, cùng cách giải quyết gọn gàng. Hai vendor, hai bản vá bump version, không lằng nhằng.
GitHub (Copilot) — đây mới là ca rối. Tại thời điểm công bố, Microsoft “chưa phát hành bản vá cho Copilot.” Nền tảng hosted của GitHub tự giảm thiểu vấn đề gốc bằng cách chặn hẳn tên branch/tag có dạng giống SHA, đóng cửa với bất cứ thứ gì host trên github.com. Nhưng đó là miếng vá ở tầng nền tảng, không phải vá Copilot — nếu plugin của bạn nằm trên Bitbucket hoặc Git server tự host, cái quy tắc chặn tên đó không áp dụng cho bạn, và bản thân Copilot vẫn chưa được vá. Dựa vào “nền tảng tình cờ chặn được” thay vì sửa logic verify của chính mình là kiểu lỗ hổng trông ổn cho đến khi ai đó trỏ một Git remote tự host vào.
Google (Gemini CLI) — câu trả lời sắc nhất trong bốn cái, mà theo hướng xấu: nó đã deprecated và sẽ không nhận bản vá nào nữa. Hướng dẫn của Google là chuyển sang Antigravity. Nếu bạn vẫn đang chạy Gemini CLI với plugin pin theo commit hash, sẽ chẳng có bản vá nào tới cả. Cách giảm thiểu duy nhất là “ngừng dùng cái này” — nghe ổn với dự án mới, nhưng là một buổi chiều thực sự tệ nếu bạn đang gắn nó vào pipeline CI ngay lúc này.
Xếp hạng cho vui: Anthropic và OpenAI ra bản vá thật, nhanh. GitHub vá nửa vời ở tầng nền tảng và để Copilot lộ tại thời điểm công bố. Google thì không vá gì cả — chỉ bảo mọi người dọn đi chỗ khác.
Tự verify hash — đừng tin cái dấu tick của công cụ
Đây là phần bạn có thể hành động ngay, trước khi bản vá của vendor tới tay bạn, hoặc nếu bạn kẹt với Gemini CLI mà không có bản vá nào sắp ra. Nguyên lý sửa khá đơn giản: đừng để cơ chế resolve reference của Git tự chọn commit giùm bạn. Resolve thẳng object và tự hash nội dung tree.
#!/usr/bin/env bash
# verify-plugin-hash.sh
# Cách dùng: ./verify-plugin-hash.sh <repo-url> <pinned-commit-sha>
set -euo pipefail
REPO_URL="$1"
PINNED_SHA="$2"
WORKDIR=$(mktemp -d)
git clone --quiet "$REPO_URL" "$WORKDIR/plugin"
cd "$WORKDIR/plugin"
# Bước 1: resolve SHA như một *object* Git thật sự, không phải ref/tên branch.
# Hậu tố ^{commit} ép Git resolve theo object và báo lỗi ngay
# nếu cái "sha" đó thật ra chỉ tồn tại dưới dạng tên branch trỏ đi chỗ khác.
RESOLVED=$(git rev-parse --verify "${PINNED_SHA}^{commit}") || {
echo "FAIL: ${PINNED_SHA} không resolve ra được commit object thật"
exit 1
}
if [ "$RESOLVED" != "$PINNED_SHA" ]; then
echo "FAIL: resolve bị lệch — kỳ vọng ${PINNED_SHA}, nhận được ${RESOLVED}"
echo "Đây chính xác là kiểu đụng độ tên branch của Plugin4Shell. Dừng lại ngay."
exit 1
fi
# Bước 2: checkout đúng commit object đó theo hash, không theo bất kỳ ref nào.
git checkout --quiet --detach "$PINNED_SHA"
# Bước 3: hash nội dung working tree thật và so với hash đã biết là "sạch"
# mà bạn ghi lại từ lần đầu tiên vet plugin này.
ACTUAL_TREE_HASH=$(git rev-parse "HEAD^{tree}")
echo "Commit: $PINNED_SHA"
echo "Tree hash: $ACTUAL_TREE_HASH"
# Tùy chọn: hash toàn bộ nội dung file để so sánh chắc ăn hơn nữa
find . -type f -not -path './.git/*' -print0 \
| sort -z \
| xargs -0 sha256sum \
| sha256sum
rm -rf "$WORKDIR"
Dòng quan trọng nhất là git rev-parse --verify "${PINNED_SHA}^{commit}". Cái hậu tố ^{commit} đó ép Git resolve chuỗi string thành object commit thật thay vì để một branch trùng tên thắng trong quá trình lookup — đúng cái sự mập mờ mà Plugin4Shell lợi dụng. Nếu SHA resolve ra không khớp với cái bạn yêu cầu, dừng ngay lập tức. Đừng checkout tiếp. Cái lệch đó chính là cuộc tấn công, bị bắt trước khi nó chạm vào filesystem của bạn.
Gắn cái này vào CI như một bước tiền xử lý trước khi cài bất kỳ plugin agent nào, và diff tree hash với giá trị bạn đã ghi lại từ lần đầu review plugin. Nếu sau này có ai đó viết lại repo upstream, tree hash của bạn sẽ đổi dù commit SHA trong file config vẫn y nguyên — vì lúc này bạn mới thực sự kiểm tra cái mình tuyên bố là đang kiểm tra.
Việc cần làm trong tuần này
- Kiểm tra version Claude Code. Nếu dưới 2.1.179, cập nhật ngay — đây là cái dễ ăn nhất.
- Kiểm tra Codex. Dưới 0.146.0 nghĩa là bạn đang lộ; lên 0.146.0 trở lên.
- Nếu bạn đang chạy Copilot với Bitbucket, Git server tự host, hoặc bất cứ thứ gì không phải nền tảng hosted của github.com, đừng mặc định là cơ chế chặn tên branch của GitHub bảo vệ được bạn. Không đâu.
- Nếu bạn vẫn dùng Gemini CLI với plugin pin theo commit, bắt đầu kế hoạch chuyển sang Antigravity ngay. Sẽ không có bản vá nào tới — “để sau” không nằm trong roadmap.
- Gắn script verify hash ở trên (hoặc phiên bản tương đương của bạn) vào bất kỳ pipeline nào tự động cài plugin agent, và ngừng tin vào một dấu tick xanh mà bạn không tự tạo ra.
Mô hình phân quyền của agent bạn chỉ trung thực bằng đúng cái tầng bên dưới nó — cái tầng quyết định byte nào thực sự được thực thi. Ngay lúc này, với ít nhất một trong bốn công cụ mà có thể bạn đang dùng, tầng đó vẫn chưa kiểm tra.