Bạn có một dịch vụ Node.js nhỏ chạy ổn định cả năm trời: nhận request, gọi fetch() sang một API bên ngoài, xử lý dữ liệu rồi trả kết quả. Bạn chưa từng viết dòng nào liên quan tới nén hay giải nén. Vậy mà sáng nay, nhóm chat kỹ thuật báo rằng chính đoạn code "vô hại" ấy có thể là cửa ngõ để kẻ xấu chạy mã trên máy chủ của bạn.
Ngày 21/10/2026, dự án Node.js phát hành bản cập nhật bảo mật vá 10 lỗi cùng lúc, trong đó nghiêm trọng nhất là lỗi trong bộ giải nén Brotli đi kèm Node.js. Bài này giải thích lỗi nằm ở đâu, vì sao ứng dụng của bạn có thể dính dù không hề gọi tới Brotli, mức độ nguy hiểm thực tế ra sao, và bạn cần nâng lên phiên bản nào.

Đợt vá này gồm những gì và ai bị ảnh hưởng?
Bản cập nhật áp dụng cho cả ba nhánh Node.js còn được hỗ trợ. Mỗi nhánh có đúng một phiên bản đã vá:
| Nhánh | Loại | Phiên bản an toàn |
|---|---|---|
| 22.x | LTS (hỗ trợ dài hạn) | 22.23.4 |
| 24.x | LTS (hỗ trợ dài hạn) | 24.22.1 |
| 26.x | Current (bản hiện hành) | 26.12.1 |
| 20.x trở về trước | Đã hết vòng đời | Không có bản vá |
Điểm cần chú ý nhất nằm ở dòng cuối: Node.js 20.x và các bản cũ hơn đã hết vòng đời (end of life) nên sẽ không nhận bản sửa. Nếu máy chủ của bạn vẫn chạy Node 18 hay Node 20, cách duy nhất là chuyển hẳn sang một nhánh còn được hỗ trợ.
Kiểm tra nhanh phiên bản đang chạy:
node -v
Phiên bản Node trên laptop của bạn chưa chắc trùng với phiên bản trong Docker image, trên CI hay trên máy chủ production. Hãy kiểm tra ở mọi nơi ứng dụng thực sự chạy.
Brotli là gì mà nằm sẵn trong Node.js?
Brotli là thư viện nén dữ liệu do Google phát triển. Rất nhiều máy chủ web dùng Brotli để nén nội dung trước khi gửi đi cho nhẹ, giống như hút chân không túi quần áo để nhét vừa vali. Bên nhận phải "bơm phồng" lại, tức là giải nén, mới đọc được nội dung.
Node.js không dùng Brotli của hệ điều hành mà đóng gói sẵn một bản sao riêng bên trong. Đây là lý do lỗi này đáng chú ý: nó nằm trong code đi kèm Node.js, không nằm trong chương trình của bạn. Node.js chạy đoạn code đó mỗi khi giải nén dữ liệu Brotli, theo hai con đường:
- Trực tiếp: code của bạn (hoặc một thư viện bạn dùng) gọi module
node:zlibđể giải nén. - Tự động: code gọi
fetch()và máy chủ bên kia trả về phản hồi nén bằng Brotli. Node tự giải nén mà bạn không cần làm gì.
Con đường thứ hai chính là cái bẫy trong tình huống mở bài. Bạn không cần biết Brotli là gì, chỉ cần dùng fetch() là đã chạm tới bộ giải nén này.
Ứng dụng bị coi là có nguy cơ khi nó giải nén dữ liệu Brotli mà kẻ tấn công kiểm soát được, ví dụ:
- Body của request gửi tới máy chủ của bạn.
- Phản hồi lấy về từ một máy chủ khác, chẳng hạn khi bạn
fetch()một URL do người dùng cung cấp hoặc một dịch vụ bên thứ ba bị chiếm quyền.
Lỗi "ghi tràn bộ nhớ" hoạt động thế nào?
Về kỹ thuật, đây là lỗi out-of-bounds write (ghi ra ngoài vùng nhớ cho phép) trong bộ giải mã Brotli, bị kích hoạt bởi dữ liệu nén bị làm sai định dạng một cách cố ý.
Hãy hình dung bạn chuẩn bị một chiếc vali vừa đúng cho số đồ đã khai báo. Nhưng chiếc túi chân không kẻ gian đưa cho bạn khi bơm phồng lại to hơn nhiều so với nhãn ghi. Bạn cứ nhét, và đồ tràn ra ngoài vali, đè lên cả đồ của người bên cạnh. Trong máy tính, "đồ của người bên cạnh" là những vùng nhớ chương trình đang dùng cho việc khác. Ghi đè đúng chỗ, kẻ tấn công có thể làm chương trình chạy theo ý mình.

Bản sửa là một bản vá từ chính dự án Brotli mà Node.js chép vào. Nó khiến bộ giải mã phát hiện dữ liệu không hợp lệ và dừng sớm hơn, thay vì cứ tiếp tục ghi vượt quá bộ đệm. Nói theo kiểu vali: kiểm tra túi trước khi mở, thấy sai là trả lại ngay.
Cùng bản sửa này đã có trong Brotli 1.2.1, 1.1.1 và 1.0.10, phát hành ngày 21/10. Phần mềm nào khác cũng đóng gói sẵn Brotli có thể cần cập nhật tương tự. Một chi tiết thú vị: thay đổi này đã được đưa vào mã nguồn Brotli từ tháng 2/2026, nhưng tới tháng 10 mới được phát hành dưới dạng bản vá bảo mật.
"Thực thi mã từ xa" có đáng sợ như tên gọi?
Dự án Node.js mô tả lỗi này (CVE-2026-111497) là có thể dẫn tới Remote Code Execution (thực thi mã từ xa), tức kẻ tấn công có thể chạy mã tuỳ ý trên hệ thống bị ảnh hưởng. Tuy vậy, mức xếp hạng chính thức là High (cao), không phải Critical (nghiêm trọng). Và những gì đã được chứng minh trên thực tế hẹp hơn nhiều so với cái tên.

Ngày 22/10, một proof-of-concept (mã khai thác mẫu) được công bố công khai. Kết quả của nó:
- Trên bản build cố tình làm yếu đi để thử nghiệm: đạt được thực thi mã.
- Trên bản build Node.js tiêu chuẩn: tác giả chỉ ghi nhận làm chương trình crash và đọc được một phần giới hạn bộ nhớ tiến trình, không thực thi được mã. Chính tác giả cho biết chưa tạo được cuộc tấn công hoạt động trên bản build thông thường.
- Trên Node.js đã vá: dữ liệu sai định dạng mà mã mẫu sử dụng bị từ chối.
- Tính tới ngày 22/10, chưa có báo cáo nào về việc lỗi bị khai thác ngoài thực tế.
Nói cách khác, cửa đã được chứng minh là có khe hở, nhưng chưa ai lách lọt người qua trên một ngôi nhà bình thường. Tuy nhiên, một lần crash cũng đủ làm dịch vụ gián đoạn, và việc đọc lén được bộ nhớ tiến trình không phải chuyện nhỏ. Mã khai thác mẫu đã công khai thì khoảng cách tới một cuộc tấn công thật chỉ còn là thời gian.
Chín lỗi còn lại đáng lo tới đâu?
Brotli chiếm hết tiêu đề, nhưng ba lỗi khác trong đợt này cũng xếp hạng High:
- Vượt kiểm tra chứng chỉ TLS bằng địa chỉ IP viết kiểu lạ (CVE-2026-106069 và CVE-2026-105483): kẻ tấn công viết địa chỉ IP của máy chủ dưới dạng không chuẩn, khiến một chứng chỉ được chấp nhận cho máy chủ mà lẽ ra nó không được phép đại diện. Với riêng CVE-2026-105483, Node.js xếp hạng cao, dù commit sửa lỗi lại mô tả nó là mức thấp và khó khai thác.
dns.resolvecắt cụt tên miền (CVE-2026-105482): khi tên miền chứa ký tự null ở giữa, hàm này cắt tên tại đó, nghĩa là thứ được tra cứu không phải thứ bạn tưởng.

Kiểm tra TLS giống như bảo vệ đối chiếu thẻ tên với khuôn mặt người vào cửa. Lỗi ở đây là bảo vệ bị lừa vì cùng một con số được viết theo kiểu chữ khác, như dùng chữ số "toàn chiều rộng" thay cho chữ số thường.
Trong nhóm mức trung bình, đáng chú ý nhất là CVE-2026-106074: khi ứng dụng được build dưới dạng startup snapshot hoặc single-executable application, hai hàm crypto.randomUUID() và crypto.randomInt() trả về cùng một giá trị ở mọi tiến trình. Bất cứ thứ gì dựa vào tính khó đoán của các giá trị này đều mất tác dụng.
Toàn bộ 10 lỗi:
| CVE | Lỗi là gì | Mức độ | Nhánh bị ảnh hưởng |
|---|---|---|---|
| CVE-2026-111497 | Ghi tràn bộ nhớ trong bộ giải mã Brotli, có thể thực thi mã từ xa theo dự án | Cao | 22.x, 24.x, 26.x |
| CVE-2026-106069 | Vượt kiểm tra tên máy chủ TLS khi máy chủ phân giải ra IP dạng không chuẩn | Cao | 22.x, 24.x, 26.x |
| CVE-2026-105483 | Vượt kiểm tra tên máy chủ TLS khi IP viết bằng chữ số toàn chiều rộng | Cao | 24.x, 26.x |
| CVE-2026-105482 | dns.resolve cắt cụt tên chứa ký tự null |
Cao | 22.x, 24.x, 26.x |
| CVE-2026-105481 | Socket keep-alive có thể xử lý phản hồi được ghi trước khi request được gửi | Trung bình | 22.x, 24.x, 26.x |
| CVE-2026-106072 | Kiểm tra chứng chỉ cho IPv4 dạng không chuẩn cho phép tên miền xác thực điểm cuối IP | Trung bình | 22.x, 24.x, 26.x |
| CVE-2026-106074 | Bộ đệm của randomUUID() và randomInt() bị dùng lại trong snapshot và ứng dụng đơn tệp |
Trung bình | 22.x, 24.x, 26.x |
| CVE-2026-106070 | ZipFile trả về dữ liệu do kẻ tấn công kiểm soát sau khi kiểm tra header thất bại | Thấp | 26.x |
| CVE-2026-106071 | Permission Model cấp quyền truy cập tệp quá rộng qua tra cứu wildcard | Thấp | 26.x |
| CVE-2026-106073 | Vượt Permission Model bằng fs.realpathSync |
Thấp | 22.x, 24.x, 26.x |
Vì sao Node.js "cấm cửa" chứng chỉ của eMudhra?
Ngoài 10 lỗi trên, bản cập nhật còn khiến Node.js ngừng tin tưởng 8 khoá của cơ quan cấp chứng chỉ trung gian thuộc nhà cung cấp Ấn Độ eMudhra, vốn được báo cáo là đã bị lộ vài ngày trước đó.

Vấn đề nằm ở cách Node.js xác minh chứng chỉ. Node dùng danh sách chứng chỉ gốc của Mozilla nhưng mặc định không kiểm tra thu hồi (revocation). Hãy hình dung một con dấu bị đánh cắp: nếu nơi nhận giấy tờ không bao giờ tra danh sách "dấu đã mất", giấy tờ đóng bằng con dấu đó vẫn được chấp nhận. Vì vậy trước bản cập nhật, một chuỗi chứng chỉ dựng trên một trong các khoá bị lộ vẫn được Node coi là hợp lệ.
Bản cập nhật xử lý theo hai hướng:
- Chặn cứng 8 khoá ngay trong Node.js.
- Cập nhật danh sách chứng chỉ gốc đi kèm, gỡ bỏ ba chứng chỉ gốc emSign.
Ngôn ngữ Go cũng đã chặn tương tự trong bản cập nhật ngày 20/10.
Các kết nối được nối lại (resume) từ một phiên bắt đầu trước khi cập nhật sẽ không bị kiểm tra lại cho tới khi phiên đó hết hạn. Và các nhánh đã hết vòng đời không nhận được lệnh chặn này.
Còn những thành phần nào được làm mới?
Node.js không chỉ là code của riêng nó mà còn đóng gói nhiều thư viện bên trong. Đợt này, các thành phần sau cũng được cập nhật:
- undici, thư viện HTTP đứng sau
fetch(): lên phiên bản sửa 3 lỗi mức trung bình, gồm một lỗi trong cơ chế kiểm tra Subresource Integrity và hai lỗi cạn kiệt bộ nhớ, trong đó một lỗi nằm ở WebSocket client tích hợp sẵn trong Node.js. - c-ares (thư viện phân giải DNS): cập nhật trên mọi nhánh.
- OpenSSL: cập nhật trên nhánh 22.x và 24.x.
- SQLite: cập nhật trên nhánh 22.x.
Các bản cập nhật thư viện này nhằm đưa vào bản sửa cho những lỗ hổng đã được công bố công khai.

Chưa nâng cấp ngay được thì làm gì?
Câu trả lời ngắn và hơi phũ: nâng cấp chính là bản sửa. Thông báo của dự án không đưa ra cách khắc phục tạm thời nào cho lỗi Brotli, nghĩa là bên trong Node.js không có công tắc nào để tắt tạm cho an toàn.
Một ví dụ về giảm thiểu từ phía hạ tầng: Vercel, nền tảng chạy Node.js cho khách hàng, cho biết đã thêm luật tường lửa từ ngày 21/10 để chặn kiểu tấn công này trong các request gửi tới. Nhưng chính Vercel nói rõ hai giới hạn:
- Luật này không bảo vệ các phản hồi Brotli mà ứng dụng tự
fetch()về từ máy chủ khác. - Nó không thay thế việc nâng cấp.

Tường lửa giống như bảo vệ đứng ở cổng trước: soát được người đi vào, nhưng không soát được những gói hàng bạn tự đặt ship về nhà. Mà con đường fetch() lại chính là "gói hàng tự đặt".
Danh sách việc cần làm:
- [ ]Chạy
node -vở mọi môi trường: máy dev, CI, Docker image, production - [ ]Nâng nhánh 22.x lên 22.23.4, 24.x lên 24.22.1, 26.x lên 26.12.1
- [ ]Còn Node 20 trở xuống thì lên kế hoạch chuyển sang nhánh còn hỗ trợ
- [ ]Rà soát chỗ nào ứng dụng
fetch()URL do người dùng cung cấp hoặc giải nén body request - [ ]Nếu ứng dụng có đóng gói Brotli riêng ngoài Node.js, kiểm tra bản cập nhật Brotli 1.2.1, 1.1.1 hoặc 1.0.10
- [ ]Nếu build ứng dụng dạng snapshot hoặc single-executable, build lại sau khi nâng cấp
Không có đường tắt cho đợt này: nâng phiên bản Node.js là cách duy nhất để vá lỗi Brotli.
Tóm tắt nhanh
- Ngày 21/10/2026, Node.js vá 10 lỗi trên ba nhánh 22.x, 24.x, 26.x. Phiên bản an toàn: 22.23.4, 24.22.1, 26.12.1. Node 20.x trở xuống không được vá.
- Lỗi nặng nhất là ghi tràn bộ nhớ trong bộ giải mã Brotli đi kèm Node, có thể bị kích hoạt chỉ qua
fetch()một phản hồi nén Brotli. - Dự án gọi đây là lỗi có thể thực thi mã từ xa, xếp hạng High. Mã khai thác mẫu trên bản build chuẩn mới chỉ gây crash và đọc được một phần bộ nhớ; chưa có khai thác ngoài thực tế tính tới 22/10.
- Bản cập nhật còn chặn 8 khoá chứng chỉ trung gian bị lộ của eMudhra và làm mới undici, c-ares, OpenSSL, SQLite.
- Không có cách khắc phục tạm thời; tường lửa chỉ chặn được một chiều. Hãy nâng cấp.
Câu hỏi thường gặp
Ứng dụng của tôi không dùng zlib, có bị ảnh hưởng không?
Có thể có. Chỉ cần ứng dụng gọi fetch() và máy chủ bên kia trả về phản hồi nén Brotli, Node.js sẽ tự giải nén bằng đúng đoạn code bị lỗi. Nguy cơ thực sự xuất hiện khi dữ liệu Brotli đó do kẻ tấn công kiểm soát.
Lỗi Brotli đã bị khai thác ngoài thực tế chưa?
Tính tới ngày 22/10/2026, chưa có báo cáo nào. Mã khai thác mẫu công khai chỉ đạt thực thi mã trên một bản build cố tình làm yếu; trên bản build chuẩn, nó gây crash và đọc được một phần giới hạn bộ nhớ.
Tôi đang dùng Node.js 20, chờ bản vá được không?
Không. Node.js 20.x và các bản cũ hơn đã hết vòng đời, sẽ không nhận bản sửa lỗi Brotli lẫn lệnh chặn chứng chỉ eMudhra. Bạn cần chuyển sang nhánh 22.x, 24.x hoặc 26.x.
Dùng tường lửa như Vercel là đủ chưa?
Chưa. Luật tường lửa của Vercel chặn tấn công trong request gửi tới, nhưng không bảo vệ phản hồi Brotli mà ứng dụng tự lấy về từ máy chủ khác, và chính Vercel nói nó không thay thế việc nâng cấp.



