Lỗ hổng symlink Claude Code lần hai: 2.1.265 vá bằng backslash, claude plugin validate vẫn im lặng y hệt bản cũ
Claude Code 2.1.265 (8/9/2026) vá thêm một lỗ hổng symlink containment trong plugin, lần này qua backslash. Tôi tự cài song song bản trước và sau bản vá bằng npm, dựng plugin test có symlink thật, chạy claude plugin validate và claude plugin install trên cả ba code path — kết quả JSON giống hệt nhau ở mọi phép thử, cho thấy bản vá nằm ở một lớp tôi không chạm tới được trong sandbox này: thời điểm session thật đọc nội dung symlink.

Trong bài này
Ngày 8/9/2026, Claude Code 2.1.265 vá thêm một lỗ hổng symlink containment khác trong hệ thống plugin — lần này khai thác qua ký tự backslash trong đường dẫn. Để kiểm chứng, tôi tự cài song song bản ngay trước bản vá (2.1.263) và bản đã vá (2.1.267, bản đang chạy chính sandbox nộp bài viết này) trên cùng một máy, dựng một plugin test thật có symlink, rồi chạy claude plugin validate trên cả hai. Kết quả JSON trả về giống hệt nhau ở cả hai bản: công cụ mà một người viết plugin sẽ chạy trước khi publish không hề chạm tới đoạn code mà bản vá này sửa.
Đây là bài thứ hai tôi viết về cùng một lớp lỗ hổng trên Claude Code, sau bài Claude Code vá lỗ hổng symlink trong plugin. Trình validate của nó vẫn cho qua sạch đăng 2/9/2026. Lần trước là lỗ hổng symlink "thường" — file symlink nằm trong commands/ hay agents/ trỏ ra ngoài thư mục plugin mà validator không cảnh báo gì. Lần này là một vector khác, mới xuất hiện trong changelog tuần này: dùng backslash để né chính cái containment check vừa được dựng lên. Hai bài không nói cùng một chuyện, nhưng cùng một câu hỏi: khi Claude Code tự nhận đã "vá", cái gì thực sự thay đổi, và cái gì thì không?
Bản vá nói gì
Tôi lấy nguyên văn từ changelog chính thức tại code.claude.com/docs/en/changelog, mục 2.1.265 (8/9/2026):
Fixed a plugin path containing a backslash bypassing the symlink containment check on macOS and Linux
Một dòng, không giải thích thêm cơ chế. Cùng bản 2.1.265 còn có một dòng liên quan khác, ít nổi bật hơn nhưng đáng chú ý vì cùng nói về việc containment check bị bỏ sót:
Fixed plugin directories whose names begin with two dots being wrongly refused as outside the plugin root
Cùng bản này còn một dòng thứ ba, cũng nói về việc plugin loader xử lý sai các trường hợp biên liên quan tới symlink, nhưng theo hướng khác — không phải escape, mà là silent failure:
Fixed a plugin's default component folder that the OS cannot check, such as a symlink loop, being silently skipped; it is now reported in
/pluginwith the error code
Đọc ba dòng cạnh nhau, bức tranh khá rõ: containment check của Claude Code — cơ chế xác định một đường dẫn (component file, symlink target, tên thư mục) có nằm trong phạm vi thư mục plugin hay không — đã bị chỉnh sửa ít nhất ba lần chỉ trong một bản phát hành, theo ba hướng khác nhau: chặt hơn với backslash (chặn escape), lỏng hơn với tên thư mục bắt đầu bằng hai dấu chấm như ..hidden-plugin (trước đó bị từ chối oan vì bị hiểu lầm là ..), và minh bạch hơn với symlink loop — trước đây bị bỏ qua âm thầm, giờ báo lỗi rõ ràng qua /plugin. Đây đúng kiểu bug quen thuộc của mọi bộ lọc path traversal viết bằng string-matching: sửa một lỗ thì dễ đụng lỗ khác, và một bản vá bảo mật thường kéo theo vài bản vá phụ để dọn lại các trường hợp biên xung quanh.
Bản 2.1.266 phát hành cùng ngày 8/9 là một bản hotfix riêng, không liên quan tới symlink — nó sửa một regression của chính 2.1.265 ở biến môi trường CLAUDE_CODE_USE_GATEWAY làm hỏng cấu hình proxy/gateway của một số người dùng. Tôi không đào sâu phần đó trong bài này vì nó lệch chủ đề, chỉ nêu để không bị hiểu nhầm là hai bug khác nhau gộp làm một.
Xâu chuỗi lại với bài trước, dòng thời gian các lần containment check trong hệ thống plugin bị đụng tới chỉ trong chín ngày là:
| Version | Ngày | Thay đổi liên quan symlink/containment |
|---|---|---|
| 2.1.257 | 1/9/2026 | Vá lỗi plugin đọc file ngoài thư mục qua symlink (theo bài trước của tôi) |
| 2.1.263 | 6/9/2026 | "Bug fixes and reliability improvements" — không có mục symlink riêng trong changelog |
| 2.1.265 | 8/9/2026 | Vá backslash bypass containment check; sửa oan sai với tên thư mục hai dấu chấm; báo lỗi rõ cho symlink loop qua /plugin |
| 2.1.267 | 9/9/2026 | Không có thay đổi symlink mới; là bản tôi dùng làm "sau vá" trong thử nghiệm này |
Ba trong bốn bản chỉ trong chín ngày có đụng tới cùng một cụm logic. Đó là lý do tôi nghĩ đáng để test lại thay vì tin luôn dòng changelog.
Hai kênh changelog, một khoảng trễ đồng bộ
Trước khi thử nghiệm, tôi đối chiếu hai nguồn changelog "chính hãng" mà Anthropic công bố song song: trang docs (code.claude.com/docs/en/changelog) và file CHANGELOG.md trong chính repo GitHub anthropics/claude-code. Tôi fetch trực tiếp:
https://raw.githubusercontent.com/anthropics/claude-code/refs/heads/main/CHANGELOG.md
Tính đến lúc tôi fetch (10/9/2026), entry mới nhất trong file này là 2.1.263, không có 2.1.265, 2.1.266, hay 2.1.267 — dù trang docs đã liệt kê đầy đủ cả ba, với 2.1.267 là bản mới nhất ghi ngày 9/9. Tức là bản vá backslash mà tôi đang viết về nó, tính tới thời điểm bài này lên nháp, chưa được mirror sang CHANGELOG.md công khai trên GitHub — nơi mà chính tôi, và có lẽ không ít agent khác, coi là nguồn "trích nguyên văn" mặc định vì nó dễ fetch qua raw URL, không qua lớp render JS nào. Đây không phải lỗi nghiêm trọng — chỉ là hai pipeline xuất bản có độ trễ khác nhau — nhưng nó là lý do thực tế tôi đổi sang lấy nguyên văn từ trang docs cho bài này, thay vì lặp lại thói quen cũ trỏ vào GitHub.
Dựng lại thử nghiệm: cài song song hai bản qua npm
Package @anthropic-ai/claude-code trên npm giữ lại toàn bộ lịch sử version, nên thay vì suy đoán, tôi cài thẳng cả bản ngay trước và ngay sau mốc vá vào hai thư mục cục bộ riêng biệt, không đụng tới bản đang chạy chính session này:
$ npm install --prefix ./v263 @anthropic-ai/[email protected]
added 2 packages in 6s
$ npm install --prefix ./v267 @anthropic-ai/[email protected]
added 2 packages in 4s
Xác nhận cả hai chạy đúng version khai báo:
$ ./v263/node_modules/.bin/claude --version
2.1.263 (Claude Code)
$ ./v267/node_modules/.bin/claude --version
2.1.267 (Claude Code)
2.1.267 cũng chính là version đang chạy sandbox nộp bài viết này qua MCP server OAuth của beru.io.vn — cùng một binary, không phải bản dựng riêng cho bài viết.
Tiếp theo tôi dựng một plugin tối giản, có manifest hợp lệ và một thư mục commands/ chứa hai symlink trỏ ra ngoài thư mục plugin: một cái dùng escape kiểu cũ, kinh điển (forward slash, ../../) để làm đối chứng — vì loại này lẽ ra đã bị chặn từ bản vá 2.1.257 theo bài trước — và một cái dùng chính kỹ thuật mà 2.1.265 nói là đã vá: target chứa backslash thay vì forward slash.
$ python3 -c "
import os
target = '..' + chr(92) + '..' + chr(92) + 'secret-outside' + chr(92) + 'leaked.md'
os.symlink(target, 'plugin-root/commands/backslash-target.md')
print('created symlink with target:', repr(target))
"
created symlink with target: '..\\..\\secret-outside\\leaked.md'
$ readlink plugin-root/commands/control-forward-slash.md
../../secret-outside/leaked.md
$ readlink plugin-root/commands/backslash-target.md
..\..\secret-outside\leaked.md
$ ls -la plugin-root/commands/
total 8
drwxr-xr-x 2 root root 4096 Sep 10 01:13 .
drwxr-xr-x 4 root root 4096 Sep 10 01:13 ..
lrwxrwxrwx 1 root root 30 Sep 10 01:13 backslash-target.md -> ..\..\secret-outside\leaked.md
lrwxrwxrwx 1 root root 30 Sep 10 01:13 control-forward-slash.md -> ../../secret-outside/leaked.md
Tôi dùng Python thay vì ln -s trực tiếp trong shell vì backslash rất dễ bị chính shell nuốt mất trước khi tới được lệnh symlink() — readlink ở trên xác nhận target thực sự được ghi đúng ký tự backslash, không bị escape lệch đi.
Kết quả: giống hệt nhau ở cả hai bản, cả trước lẫn sau bản vá
Bước đầu tiên, tôi validate toàn bộ plugin — trỏ claude plugin validate vào thư mục gốc, đúng cách một người viết plugin sẽ làm trước khi publish:
$ ./v263/node_modules/.bin/claude plugin validate ./plugin-root --json
{
"success": true,
"strict": false,
"target": ".../plugin-root/.claude-plugin/plugin.json",
"manifest": { "type": "plugin", "errors": [], "warnings": [...], "notes": [] },
"contents": []
}
$ ./v267/node_modules/.bin/claude plugin validate ./plugin-root --json
{
"success": true,
"strict": false,
"target": ".../plugin-root/.claude-plugin/plugin.json",
"manifest": { "type": "plugin", "errors": [], "warnings": [...], "notes": [] },
"contents": []
}
"contents": [] ở cả hai bản. Không một dòng nào nhắc tới hai symlink nằm trong commands/. Đây chính là khoảng trống tôi đã mô tả trong bài trước, và nó vẫn còn nguyên — kể cả sau khi 2.1.265 đã vá phần backslash ở nơi khác trong codebase. Với plugin có manifest đầy đủ, claude plugin validate chưa bao giờ đi vào từng thư mục component để kiểm tra symlink cả; nó chỉ validate cấu trúc manifest. Đây là hành vi không đổi qua bản vá, không phải hồi quy — chỉ là container check của phiên vá này không nằm trong lộ trình mà lệnh validate đi qua.
Bước hai, tôi thử trỏ validate thẳng vào thư mục commands/ — cách dùng khác, theo đúng mô tả trong --help: "Validate a plugin or marketplace manifest, or the skills, agents, and commands in a directory". Ở chế độ này validate có nhìn thấy symlink, nhưng câu trả lời cũng giống hệt nhau ở cả hai bản:
$ ./v263/node_modules/.bin/claude plugin validate ./plugin-root/commands --json
{
"success": true,
"target": ".../plugin-root/commands",
"manifest": null,
"contents": [{
"file": ".../plugin-root/commands",
"type": "command",
"errors": [],
"warnings": [{
"path": "directory",
"message": "2 entries here are symlinks and were not read — components are read without following symlinks. A session loading this directory does follow them, so validate the real paths separately.",
"code": null
}],
"notes": []
}]
}
./v267/... trả về đúng cùng một object, chỉ khác đường dẫn tuyệt đối. Không có gì phân biệt symlink backslash với symlink forward-slash bình thường — cả hai bị gộp chung vào con số "2 entries" — và quan trọng hơn, chính message của tool tự thú nhận điều tôi nghi ngờ: validate không đọc target của bất kỳ symlink nào, dù cũ hay mới, dù escape kiểu nào. Việc symlink có thực sự "follow" ra ngoài thư mục hay không chỉ được quyết định khi một session thật load plugin đó — và đó chính xác là nơi bản vá 2.1.265 nói nó đã sửa.
Nói cách khác: tool CLI dành cho người viết plugin tự kiểm tra trước khi publish (claude plugin validate) hoàn toàn tách biệt khỏi code path mà bản vá bảo mật 2.1.265 chạm vào (runtime khi một session nạp plugin). Một plugin author chạy validate, thấy pass sạch hoặc chỉ thấy một cảnh báo chung chung không phân biệt được mức độ nguy hiểm, sẽ không có cách nào biết plugin của chính mình — hoặc plugin họ định cài từ một marketplace lạ — có đang dùng chiêu backslash mà 2.1.265 vừa vá hay không, kể cả trên chính bản 2.1.267 đã vá.
Đi xa hơn: cài plugin thật qua marketplace cục bộ
validate không phải cách duy nhất người dùng chạm vào một plugin trước khi tin nó. Cách phổ biến hơn là claude plugin install. Tôi dựng thêm một marketplace cục bộ (không qua mạng, không qua GitHub) để xem bước copy file lúc install có xử lý hai symlink khác đi so với validate hay không — và có khác nhau giữa 2.1.263 với 2.1.267 hay không.
Theo đúng schema marketplace mà tôi lấy từ tài liệu chính thức — trường source phải là chuỗi bắt đầu bằng ./ và không được trỏ ra ngoài root của marketplace bằng ../ — tôi đặt plugin test làm thư mục con của chính marketplace:
{
"name": "test-marketplace",
"owner": {"name": "beru-test"},
"plugins": [
{"name": "acme-test-plugin", "source": "./plugin-root", "description": "test plugin for symlink containment"}
]
}
Tôi chạy toàn bộ trong HOME biệt lập (export HOME=$PWD/fake-home) để không đụng vào cấu hình thật của session đang nộp bài này, rồi thêm marketplace và cài plugin trên bản 2.1.267:
$ claude plugin marketplace add ./marketplace-src --scope local
Adding marketplace…√ Successfully added marketplace: test-marketplace (declared in local settings)
$ claude plugin install acme-test-plugin@test-marketplace --scope local -y
Installing plugin "acme-test-plugin@test-marketplace"...√ Successfully installed plugin: acme-test-plugin@test-marketplace (scope: local)
Install thành công, và quan trọng hơn — nó thật sự copy plugin ra một thư mục cache riêng, không chạy từ plugin-root gốc nữa. Tôi soi thẳng vào thư mục cache đó:
$ find fake-home/.claude/plugins/cache/test-marketplace/acme-test-plugin
.../unknown
.../unknown/.claude-plugin
.../unknown/.claude-plugin/plugin.json
.../unknown/commands
.../unknown/commands/backslash-target.md
$ ls -la .../unknown/commands
total 8
lrwxrwxrwx 1 root root 30 Sep 10 01:19 backslash-target.md -> ..\..\secret-outside\leaked.md
Hai điều đáng chú ý cùng lúc. Thứ nhất: control-forward-slash.md — symlink escape kiểu cũ, ../../ — biến mất hoàn toàn khỏi bản cài. Bước install đã tự lọc bỏ nó, không copy, không báo lỗi ồn ào, chỉ đơn giản là không có mặt trong thư mục cache. Thứ hai: backslash-target.md thì được giữ lại y nguyên dưới dạng symlink, target vẫn còn nguyên chuỗi backslash — không bị resolve, không bị chặn, không bị cảnh báo gì trong output của lệnh install.
Tôi lặp lại chính xác quy trình đó trên bản 2.1.263 (dùng một HOME biệt lập khác để không lẫn state):
$ find fake-home-263/.claude/plugins/cache/test-marketplace/acme-test-plugin
.../unknown/.claude-plugin/plugin.json
.../unknown/commands
.../unknown/commands/backslash-target.md
$ ls -la .../unknown/commands
lrwxrwxrwx 1 root root 30 Sep 10 01:19 backslash-target.md -> ..\..\secret-outside\leaked.md
Giống hệt 2.1.267, từng bit một: control-forward-slash.md bị lọc bỏ, backslash-target.md được giữ nguyên dạng symlink chưa resolve. Tức là bước copy file lúc install — thêm một code path nữa, khác với cả hai chế độ validate đã test ở trên — cũng không đổi gì giữa bản trước và sau bản vá 2.1.265. Ba code path tôi test được (validate toàn plugin, validate một thư mục con, và install copy file) đều cho kết quả giống hệt nhau giữa 2.1.263 và 2.1.267. Bản vá, nếu có tác dụng thật, phải nằm ở một chỗ thứ tư mà tôi chưa chạm tới được: thời điểm một session thật sự đọc nội dung của backslash-target.md để dùng làm slash command.
Một chi tiết phụ đáng ghi lại: sau khi install xong, tôi chạy claude plugin details acme-test-plugin để xem "component inventory" mà Claude Code tự thống kê cho plugin này — công cụ này lẽ ra là nơi hợp lý nhất để phát hiện một component bất thường trước khi bật plugin lên.
$ claude plugin details acme-test-plugin
acme-test-plugin
Description: test plugin for symlink containment
Source: acme-test-plugin@test-marketplace
Component inventory
Skills (0)
Agents (0)
Hooks (0)
MCP servers (0)
LSP servers (0)
Projected token cost
Always-on: ~0 tok added to every session
Chú ý: danh sách "Component inventory" liệt kê Skills, Agents, Hooks, MCP servers, LSP servers — nhưng không có mục Commands nào cả, dù plugin của tôi có đúng một file trong commands/ (chính là backslash-target.md, symlink còn nguyên). Tôi không chắc đây là do commands không nằm trong nhóm "component" mà lệnh details thống kê chi phí token, hay do bản thân file đó là symlink nên bị bỏ qua khỏi phép đếm — hai khả năng dẫn tới cùng một kết quả quan sát được, và tôi không có cách phân biệt chúng chỉ bằng black-box testing. Dù lý do là gì, kết quả thực tế vẫn vậy: người dùng gõ claude plugin details để soi trước khi bật một plugin lạ sẽ thấy đúng năm dòng "(0)" và một dòng chi phí token bằng 0, không có gợi ý nào rằng có một symlink nghi vấn đang nằm trong thư mục vừa cài.
Giới hạn tôi không vượt qua được
Tôi muốn đi xa hơn nữa: thử nạp plugin thật vào một session (claude --plugin-dir ./plugin-root, hoặc dùng chính plugin đã cài ở trên) để xem containment check ở thời điểm load — nơi bản vá 2.1.265 nhiều khả năng thực sự nằm, vì ba code path tôi vừa test đều không đổi — có chặn được symlink backslash hay không, so với bản 2.1.263 chưa vá. Đây là phép thử duy nhất có thể trả lời dứt khoát câu hỏi "lỗ hổng cũ có thật sự leak được file ngoài thư mục hay không".
Tôi không làm được việc này trong sandbox đang chạy bài viết. Một agent khác của tôi, khi viết bài về managedMcpServers hồi đầu tháng 9, đã ghi lại việc bị chính rule Containment Escape của Claude Code chặn khi thử lồng một session con bên trong session đang chạy. Tôi gặp đúng giới hạn đó: khởi động một claude session mới (dù trỏ vào bản 2.1.263 hay 2.1.267 cài cục bộ) từ bên trong sandbox đang nộp bài này để tự nó gọi model, đi ngược lại chính sách containment của phiên đang chạy — hợp lý về mặt bảo mật, nhưng cũng có nghĩa: tôi xác nhận được chỗ 2.1.265 KHÔNG sửa (validate), nhưng không tự tay xác nhận được chỗ nó CÓ sửa (session load) bằng cách đọc thật một file bị leak. Tôi ghi rõ điều này ở đây thay vì giả vờ đã kiểm chứng đầy đủ.
Điều tôi có thể nói chắc, dựa trên phần đã test: containment check ở cấp filesystem thuần tuý (readlink, ls -la phía trên) hoạt động đúng như POSIX — backslash trên Linux/macOS không phải ký tự phân tách đường dẫn, nó chỉ là một ký tự thường nằm trong tên file. Symlink target ..\..\secret-outside\leaked.md không tự động được hệ điều hành "hiểu" thành ../../secret-outside/leaked.md. Vậy bug nằm ở tầng ứng dụng: rất có khả năng logic containment check cũ của Claude Code tự phân tách chuỗi path bằng cả / lẫn \ (một thói quen thường thấy trong code cố viết "cross-platform" bằng regex thay vì dùng thư viện path chuẩn của Node theo đúng platform), khiến nó hiểu sai cấu trúc thư mục theo kiểu Windows ngay trên Linux/macOS. Đây là suy luận dựa trên hành vi POSIX đã kiểm chứng, không phải khẳng định về source code thật của Anthropic — tôi không có quyền truy cập source, và changelog cũng không nói chi tiết cơ chế.
Kết quả thử install ở trên củng cố thêm phần suy luận này chứ không mâu thuẫn: bộ lọc lúc copy file rõ ràng có nhận diện được ../../ (nó lọc bỏ control-forward-slash.md sạch sẽ, trên cả hai bản), nhưng cùng bộ lọc đó lại để backslash-target.md lọt qua nguyên vẹn, trên cả hai bản. Một bộ lọc "biết" chặn kiểu escape bằng forward slash nhưng "không biết" chặn kiểu escape ký tự tương đương bằng backslash, trên đúng hai bản cách nhau bởi một bản vá tuyên bố đã sửa chính chuyện đó — là bằng chứng gián tiếp khá chắc rằng bản vá 2.1.265 không đụng tới bộ lọc copy-file này, mà đụng tới một lớp kiểm tra khác, muộn hơn, chỉ chạy khi nội dung symlink thực sự được đọc.
Vì sao chuyện này liên quan tới công việc hàng ngày, không chỉ tới một CVE
Tôi không làm security research toàn thời gian. Claude Code là công cụ tôi dùng mỗi ngày để code các dự án full-stack thật — hiện có 18 project case study tại beru.io.vn/projects, từ Nuxt 3, Vue tới React, cộng với việc phát triển sản phẩm AI theo dịch vụ AI Product Development tôi nhận làm. Plugin ecosystem của Claude Code — marketplace bên thứ ba, skill tự cài, agent tự viết — là bề mặt tấn công thực sự với bất kỳ ai cài plugin lạ vào máy đang có quyền đọc/ghi code khách hàng. claude plugin validate là công cụ duy nhất một người bình thường có thể chạy để tự kiểm tra trước khi tin tưởng một plugin, và hai lần liên tiếp (bài trước với file symlink thường, bài này với symlink backslash) tôi thấy nó không phải là công cụ đủ để trả lời câu hỏi "plugin này có đang trỏ ra ngoài thư mục của nó không".
Kết luận thực tế, không phải khuyến nghị chung chung: nếu một plugin từ marketplace lạ có bất kỳ symlink nào trong commands/, agents/, skills/, hay hooks/, đừng dựa vào claude plugin validate passed để kết luận nó an toàn — vì như output ở trên cho thấy, validate hoặc bỏ qua hoàn toàn (contents: [] khi validate cả plugin), hoặc chỉ cảnh báo chung chung không phân biệt loại escape ("N entries are symlinks and were not read" khi validate riêng một thư mục con). claude plugin install cũng không phải chỗ đáng tin hơn: nó có lọc bỏ escape kiểu forward-slash, nhưng để lọt backslash, như tôi vừa chứng minh trên cả hai bản.
Quy trình thực tế tôi tự đặt ra cho mình sau hai bài này, trước khi cài bất kỳ plugin nào không phải do Anthropic hay chính tôi viết:
find <plugin-dir> -type l -exec ls -la {} \;— liệt kê toàn bộ symlink trong plugin, không chỉ trongcommands/mà cảagents/,skills/,hooks/.readlink <symlink>từng cái, đọc target bằng mắt — không tin vào việcvalidatehayinstallđã "chặn hộ" escape, vì cả hai công cụ này đều đã cho thấy có ít nhất một kiểu escape lọt qua.- Nếu target có bất kỳ ký tự lạ nào ngoài path bình thường — backslash, ký tự Unicode nhìn giống
/hoặc., khoảng trắng ẩn — coi đó là dấu hiệu cố tình né một bộ lọc string-matching nào đó, kể cả khi không chắc lọc cái gì. - Theo dõi changelog của chính Claude Code, không chỉ phần "New Features" mà cả phần "Fixed" — phần lớn các lỗ hổng containment tôi tìm thấy trong hai bài này đều nằm lẫn trong danh sách bug fix dài dằng dặc, không có mục riêng "Security".
Đây cũng là cách tiếp cận tôi áp dụng khi đánh giá bất kỳ công cụ agent nào có quyền đọc/ghi file trước khi đưa vào workflow của dịch vụ AI Product Development — không tin lời tuyên bố "đã vá" cho tới khi tự tay chạy lại được, và nói rõ phần nào chưa tự chạy lại được.
Những gì tôi không kiểm chứng được
- Tôi loại trừ được ba code path —
claude plugin validatetrên toàn plugin,claude plugin validatetrên riêng thư mục component, và bước copy file củaclaude plugin install— vì cả ba đều cho kết quả giống hệt nhau giữa 2.1.263 và 2.1.267. Nhưng tôi không xác nhận được containment check ở thời điểm session thật sự load plugin và đọc nội dung symlink — nơi bản vá 2.1.265 nhiều khả năng nhất đang nằm — có thực sự chặn được symlink backslash trên bản 2.1.263 hay không. Tức là tôi không có bằng chứng trực tiếp rằng lỗ hổng gốc từng leak được nội dung file thật ra ngoài, chỉ có bằng chứng rằng ba lớp phòng thủ sớm hơn không chặn nó. - Lý do tôi không làm được bước cuối: khởi động một session
claudemới (dù trỏ vào bản 2.1.263 hay 2.1.267 cài cục bộ) từ bên trong sandbox đang nộp chính bài viết này để tự nó gọi model, đi ngược lại chính sách containment của phiên đang chạy — một agent khác của tôi đã gặp đúng giới hạn này khi viết bài vềmanagedMcpServersđầu tháng 9. Tôi không cố lách qua giới hạn đó. - Tôi không có source code của Claude Code, nên suy luận về việc containment check cũ dùng string-splitting cross-platform là suy luận dựa trên hành vi quan sát được (POSIX không coi backslash là separator, và bộ lọc install phân biệt được forward-slash nhưng không phân biệt được backslash), không phải khẳng định đã đọc code.
- Tôi không kiểm tra hành vi tương tự trên Windows — changelog ghi rõ lỗ hổng này chỉ áp dụng cho macOS và Linux ("bypassing the symlink containment check on macOS and Linux"), nên tôi không mở rộng phạm vi sang Windows.
- Tôi không kiểm tra được liệu marketplace chính thức của Anthropic có quét symlink trước khi duyệt plugin lên marketplace hay không — marketplace cục bộ tôi dựng trong bài này chạy hoàn toàn offline, không qua bước duyệt hay review nào của bên thứ ba.
Nguồn đã fetch trong bài: Claude Code changelog, CHANGELOG.md trên GitHub, npm registry package @anthropic-ai/claude-code.
Viết và kiểm chứng bởi Nguyễn Hoàng Lộc (Beru), TP. Hồ Chí Minh
