Logo
Featured Image

Node2Shell CVE-2026-111497: Lỗ hổng RCE trong Node.js đã bị khai thác — Kiểm tra và vá ngay

Author
Tobi • 28/10/2026 • 4 views

Nếu tuần trước bạn vừa vá PHP2Shell xong và thở phào, thì tuần này có thêm một việc nữa: kiểm tra phiên bản Node.js trên server của bạn. Chỉ một tuần sau bản vá PHP, một lỗ hổng nghiêm trọng khác trong bộ giải mã Brotli — lần này nằm trong chính Node.js — đã được CISA xác nhận là đang bị khai thác thực tế. Tên của nó là Node2Shell, mã CVE-2026-111497, và cách vá thì rất đơn giản: nâng cấp Node.js lên bản đã vá rồi khởi động lại ứng dụng.

Bài này giải thích Node2Shell là gì, ai bị ảnh hưởng, cách kiểm tra server của bạn trong 2 phút và cách vá từng bước. Mình đã chạy thử toàn bộ lệnh kiểm tra trong bài trên một máy Ubuntu 24.04 để bạn thấy kết quả thật trông như thế nào.

1. Chuyện gì đang xảy ra?

Ngày 21/10/2026, Node.js phát hành bản cập nhật bảo mật cho cả ba nhánh đang được hỗ trợ, vá tổng cộng 10 lỗ hổng. Nghiêm trọng nhất là CVE-2026-111497: lỗi ghi tràn bộ nhớ (out-of-bounds write) trong bộ giải mã Brotli mà Node.js tích hợp sẵn. Node.js tự đánh giá mức High và nói rõ lỗi này "cho phép thực thi mã từ xa" (remote code execution — RCE). Google, với vai trò cơ quan cấp mã CVE, chấm CVSS 4.0: 9.2/10 (Critical).

Lỗi này do nhóm OpenAI Codex Security (Khai Tran và Sylvie Mayer) báo cáo — họ tìm ra nó khi đang thử thoát khỏi sandbox huấn luyện của chính OpenAI. Nghe có quen không? Đúng vậy: PHP2Shell tuần trước cũng được phát hiện với sự hỗ trợ của AI. Tháng 10/2026 đang trở thành tháng mà AI vừa tìm lỗi, vừa khiến lỗi lan nhanh hơn bao giờ hết.

Diễn biến sau đó rất nhanh. Chỉ vài giờ sau bản vá, các PoC công khai đã xuất hiện trên GitHub. Ngày 22/10, Imperva công bố nghiên cứu đặt tên Node2Shell và chứng minh chạy được mã trên image Node.js 24.22.0 chính thức. Và ngày 27/10/2026, CISA đưa CVE-2026-111497 vào danh mục Known Exploited Vulnerabilities (KEV) — tức là đã có bằng chứng khai thác thực tế — với hạn khắc phục 10/11/2026 cho các cơ quan liên bang Mỹ. Bạn không thuộc diện bị CISA bắt buộc, nhưng mốc 10/11 là thước đo tốt cho mức độ khẩn cấp: chỉ 2 tuần từ khi vào KEV.

Minh họa isometric máy chủ Node.js với khối lục giác xanh phát sáng

2. Node2Shell là gì? Giải thích không cần đọc code C

Brotli là một định dạng nén dữ liệu, thường dùng để nén nội dung HTTP (bạn sẽ thấy header Content-Encoding: br). Node.js tích hợp sẵn bộ giải mã Brotli để phục vụ module node:zlib. Lỗi nằm ở chỗ: khi một luồng Brotli báo hiệu "chuyển loại block" mà cấu trúc của chính nó khiến việc chuyển đổi đó không hợp lệ, bộ giải mã lẽ ra phải báo lỗi định dạng và dừng lại. Nhưng bản lỗi lại hiểu nhầm tình huống này thành "thiếu dữ liệu, cần thêm input" — thế là nó cứ tiếp tục nhận byte và ghi đè qua cuối một bộ đệm nội bộ chỉ 8 byte.

Kết quả: kẻ tấn công gửi một luồng Brotli được chế tạo đặc biệt, làm hỏng bộ nhớ của tiến trình Node.js, và tùy vào bố cục bộ nhớ lúc đó có thể gây crash, rò rỉ bộ nhớ, hoặc chạy mã tùy ý. Điểm mấu chốt: kẻ tấn công không cần đăng nhập, chỉ cần ứng dụng của bạn giải mã dữ liệu Brotli do chúng kiểm soát.

Một lưu ý trung thực: không phải PoC nào cũng chạy được mã. Nhiều PoC công khai chỉ làm crash tiến trình (tức là gây DoS), và Imperva cũng phải thử nhiều lần mới chạy được mã trên một cấu hình cụ thể. Nhưng "chỉ crash được" không phải là lý do để trì hoãn — một lỗi ghi tràn bộ nhớ trong bộ phận xử lý request là thứ không được phép tồn tại trên server production, và CISA đã xác nhận có khai thác thực tế.

3. Ai bị ảnh hưởng?

Lỗi nằm trong runtime Node.js, không nằm trong code ứng dụng hay package npm của bạn. Vì vậy npm update không vá được gì cả — bạn phải nâng cấp chính Node.js.

Minh họa isometric cảnh báo lỗ hổng trên máy chủ và lá chắn bảo vệ

Ba nhánh được hỗ trợ đều bị ảnh hưởng, và các nhánh đã hết hỗ trợ (20.x, 25.x, 23.x trở xuống) không có bản vá — Node.js tuyên bố các nhánh hết hỗ trợ luôn được coi là bị ảnh hưởng mỗi khi có bản vá bảo mật. Nếu bạn còn ở 20.x, đây chính là lúc phải lên kế hoạch nâng nhánh, giống như trường hợp PHP 8.1 trở xuống tuần trước.

Quan trọng hơn: bạn không cần gọi trực tiếp API Brotli mới bị ảnh hưởng. Có ba đường dẫn phổ biến mà dữ liệu Brotli của kẻ tấn công có thể chạm tới bộ giải mã:

  • Trực tiếp qua node:zlib: ứng dụng truyền dữ liệu không đáng tin vào zlib.brotliDecompress() hoặc zlib.createBrotliDecompress().
  • Qua fetch() tích hợp sẵn: Node.js tự động giải nén response có Content-Encoding: br. Nếu ứng dụng của bạn fetch dữ liệu từ URL do người dùng kiểm soát (webhook, crawler, proxy, tích hợp bên thứ ba...), một server độc hại hoặc bị chiếm quyền có thể tấn công ngược lại ứng dụng của bạn.
  • Qua Express 5 / body-parser 2: các parser json, raw, text, urlencoded mặc định giải nén body request có Content-Encoding: br (tùy chọn inflate mặc định là true). Bất kỳ client nào cũng có thể gửi request như vậy — đây là đường tấn công trực diện nhất đối với API public.

Ngược lại, một HTTP server Node.js thuần túy không tự giải mã body Brotli — việc giải mã là trách nhiệm của ứng dụng. Vì vậy mức độ rủi ro thực tế của bạn phụ thuộc vào việc ứng dụng có giải mã dữ liệu Brotli từ nguồn không đáng tin hay không. Nhưng đừng dựa vào đó để trì hoãn: bản vá còn sửa thêm lỗi bypass kiểm tra hostname TLS và lỗi cắt tên miền DNS — nâng cấp luôn là lựa chọn đúng.

Một lưu ý nữa: cùng một lỗi Brotli còn xuất hiện ở nhiều phần mềm khác nhúng thư viện Brotli của Google (Bun, Python, .NET, Envoy, Istio...), mỗi bên có bản vá riêng. Bài này tập trung vào Node.js; nếu bạn dùng các runtime khác, hãy kiểm tra advisory của từng nhà cung cấp.

4. Cách kiểm tra server của bạn trong 2 phút

SSH vào server và chạy các lệnh sau. Mình đã chạy thử toàn bộ trên một máy Ubuntu 24.04 — máy này đang chạy Node v24.22.1, tức là đúng bản đã vá, nên bạn sẽ thấy kết quả "OK" mẫu dưới đây.

4.1. Xem phiên bản Node.js đang chạy

node -v node -p "process.versions.node"

Đối chiếu với bảng ở mục 3. Nếu bạn đang ở 22.x dưới 22.23.4, 24.x dưới 24.22.1, hoặc 26.x dưới 26.12.1 — bạn đang bị ảnh hưởng. Nếu ở 20.x trở xuống — bạn đang ở nhánh hết hỗ trợ, cần nâng nhánh.

4.2. Chạy script kiểm tra tự động

Để khỏi tra bảng bằng tay, lưu đoạn script sau thành kiem-tra-node.js rồi chạy node kiem-tra-node.js. Script chỉ đọc phiên bản hiện tại và đối chiếu với bảng bản vá, không gửi gì ra ngoài:

const fixed = { '22': '22.23.4', '24': '24.22.1', '26': '26.12.1' }; const v = process.version.replace(/^v/, ''); const major = v.split('.')[0]; if (!fixed[major]) { console.log('CANH BAO: Node ' + process.version + ' thuoc nhanh het ho tro.'); console.log('Khong co ban va chinh thuc - can chuyen sang 22, 24 hoac 26.'); } else if (compare(v, fixed[major]) >= 0) { console.log('OK: Node ' + process.version + ' da dat ban va ' + fixed[major] + '.'); } else { console.log('CHUA VA: Node ' + process.version + ' - can cap nhat len ' + fixed[major] + '.'); } function compare(a, b) { const pa = a.split('.').map(Number), pb = b.split('.').map(Number); for (let i = 0; i < 3; i++) { if (pa[i] !== pb[i]) return pa[i] - pb[i]; } return 0; }

Ảnh terminal chạy lệnh node -v và script kiểm tra phiên bản Node.js trên máy Ubuntu giả lập, kết quả đã vá

Trong lần chạy thử của mình, máy báo OK: Node v24.22.1 da dat ban va 24.22.1. Dòng cuối trong ảnh cũng chứng minh module node:zlib đang hoạt động — chính là bộ phận chứa lỗi trước khi vá.

4.3. Đừng quên các Node.js "ẩn" — đây mới là phần khó

Kiểm tra node -v xong chưa đủ. Trên một server thực tế, Node.js có thể tồn tại ở nhiều nơi: nvm, pm2, systemd unit với PATH hard-code, cron job trỏ tới đường dẫn tuyệt đối, container Docker cũ. Đổi Node mặc định của nvm không tự động đổi những chỗ này. Hãy rà soát:

which -a node ls -la $(which node) # cac tien trinh Node dang chay va binary that cua chung: ps aux | grep -i node | grep -v grep ls -l /proc/$(pgrep -f "node" | head -1)/exe 2>/dev/null # container Docker dang chay image Node cu: docker ps --format '{{.Names}} {{.Image}}' 2>/dev/null | head -20

Ảnh terminal liệt kê các vị trí cài đặt Node.js trên máy Ubuntu giả lập để rà soát bản chưa vá

Trên máy thử nghiệm của mình, chỉ có một Node duy nhất ở /usr/bin/node và nó đã là bản vá. Trên server của bạn, nếu thấy thêm bản ở ~/.nvm/versions/node/, /usr/local/bin/ hay trong container, hãy kiểm tra từng bản một bằng đường dẫn đầy đủ, ví dụ ~/.nvm/versions/node/v20.19.0/bin/node -v.

5. Cách vá từng bước

Nguyên tắc chung: nâng cấp runtime, khởi động lại mọi tiến trình, rồi xác minh lại bằng binary thật. Chỉ đổi phiên bản mặc định mà không restart thì tiến trình cũ vẫn chạy code cũ.

5.1. Dùng nvm (phổ biến nhất trên VPS dev)

nvm install 24 --latest-npm nvm use 24 nvm alias default 24 node -v # phai hien v24.22.1 tro len

Sau đó restart từng ứng dụng: pm2 restart all nếu dùng pm2 (kiểm tra lại bằng pm2 list và pm2 describe <app> xem đường dẫn Node đã đổi chưa), systemctl restart <service> nếu dùng systemd. Với pm2, lưu ý: pm2 daemon và các app đã lưu (saved) có thể vẫn trỏ tới binary cũ — hãy kiểm tra pm2 save sau khi restart và đối chiếu đường dẫn trong ~/.pm2/dump.pm2 nếu cần.

5.2. Ubuntu / Debian (cài từ NodeSource hoặc kho distro)

sudo apt update sudo apt install --only-upgrade nodejs node -v

Nếu kho distro chưa có bản vá (distro thường chậm hơn upstream vài ngày), hãy chuyển sang kho NodeSource cho đúng nhánh, hoặc cài binary chính thức từ nodejs.org. Sau khi nâng cấp, restart lại service như mục 5.1.

>

5.3. Docker — rebuild image, đừng chỉ restart container

# trong Dockerfile, pin tag da va thay vi tag chung chung: FROM node:24.22.1-alpine # thay cho node:24 docker build --pull -t myapp:latest . docker compose up -d --force-recreate

Tag node:22, node:24, node:26 đã được cập nhật lên bản vá, nhưng image cũ đã build vẫn chứa runtime cũ — bắt buộc phải --pull và redeploy. Tag node:20 sẽ không bao giờ nhận bản vá này.

5.4. Biện pháp tạm thời nếu chưa vá được ngay

Nếu bạn dùng Express 5 và chưa thể nâng cấp Node ngay trong ngày, hãy tắt giải nén body Brotli tạm thời (request nén sẽ bị từ chối với lỗi 415 thay vì được giải mã):

app.use(express.json({ inflate: false })); app.use(express.urlencoded({ extended: true, inflate: false }));

Đây chỉ là biện pháp mua thời gian cho một đường tấn công (body request). Nó không bảo vệ đường fetch() hay node:zlib trực tiếp, và không thay thế bản vá. Ngoài ra, hãy hạn chế truy cập vào service Node.js chỉ từ những nguồn cần thiết, và theo dõi log để phát hiện tiến trình Node bất ngờ spawn shell hoặc tải tool lạ.

5.5. Xác minh sau khi vá

node -v node sandbox/kiem-tra-node.js # hoac chay lai script o muc 4.2 # xac minh tien trinh dang chay dung binary moi: ls -l /proc/$(pgrep -f "node" | head -1)/exe

Nếu /proc/.../exe vẫn trỏ tới binary cũ, nghĩa là tiến trình đó chưa được restart — hãy restart nó ngay.

6. Kết luận

Node2Shell là lời nhắc thứ hai trong cùng một tháng rằng: lỗ hổng nguy hiểm nhất không nằm trong code bạn viết, mà nằm trong runtime bạn chạy. Tuần trước là PHP, tuần này là Node.js, và cả hai đều có chung một kịch bản: lỗi ở tầng thấp, khai thác không cần đăng nhập, bản vá có sẵn, và kẻ xấu thì không chờ bạn rảnh mới ra tay — CISA đã xác nhận khai thác thực tế chỉ 6 ngày sau bản vá.

Việc cần làm hôm nay chỉ có ba bước: chạy node -v trên mọi server, nâng cấp lên 22.23.4 / 24.22.1 / 26.12.1 (hoặc nâng nhánh nếu bạn còn ở 20.x), và restart mọi tiến trình rồi xác minh lại. Năm phút cho mỗi server, đổi lại một đêm ngủ ngon trước ngày 10/11 — mốc mà ngay cả các cơ quan liên bang Mỹ cũng phải hoàn thành.

Nếu bạn đã vá xong, hãy chia sẻ bài này cho đồng nghiệp còn chạy Node.js — nhất là những ai đang chạy Express 5 public API mà chưa biết mình đang tự động giải nén body Brotli.

7. Câu hỏi thường gặp (FAQ)

7.1. Chỉ cập nhật npm package có vá được Node2Shell không?

Không. Lỗi nằm trong runtime Node.js (bộ giải mã Brotli tích hợp sẵn), không nằm trong package npm nào. npm update không thay runtime. Bạn phải nâng cấp chính Node.js và restart tiến trình.

7.2. Ứng dụng của mình không dùng Brotli thì có an toàn không?

Chưa chắc. Bạn có thể không gọi trực tiếp API Brotli, nhưng fetch() tích hợp sẵn tự động giải nén response Brotli, và Express 5 tự động giải nén body request Brotli. Nếu ứng dụng fetch URL do người dùng kiểm soát hoặc nhận request từ client lạ, bạn vẫn có bề mặt tấn công. Hơn nữa bản vá còn sửa thêm lỗi TLS và DNS — cứ nâng cấp.

7.3. Node.js 20.x có bản vá không?

Không. 20.x đã hết hỗ trợ và không nhận bản vá cho CVE này. Cách duy nhất là nâng lên nhánh 22, 24 hoặc 26 (khuyến nghị 24 LTS cho production).

7.4. Node2Shell có liên quan gì tới giao thức HTTP/2 không?

Không. Tên gọi "Node2Shell" (và "HTTP2Shell" ở một số bài viết) dễ gây hiểu lầm, nhưng lỗi nằm ở bộ giải mã Brotli — định dạng nén — chứ không phải giao thức HTTP/2. Đừng vì tên gọi mà bỏ qua nếu bạn không dùng HTTP/2.

7.5. CISA đưa vào KEV nghĩa là gì với mình?

KEV là danh mục các lỗ hổng mà CISA có bằng chứng đang bị khai thác thực tế. Hạn 10/11/2026 chỉ bắt buộc với cơ quan liên bang Mỹ, nhưng với bạn đó là tín hiệu ưu tiên: đây không còn là lỗi lý thuyết, hãy vá trước khi các đợt quét tự động tìm tới server của bạn.

8. Nguồn tham khảo

Bình luận (0)

+ =
Zalo