Đánh giá Node.js và Deno dưới khía cạnh bảo mật

1. Vấn đề bảo mật của Node.js
Khi chạy một ứng dụng Node, nó sẽ có quyền truy cập vào mọi thứ từ hệ thống tập tin, internet, biến môi trường.
Không chỉ bản thân ứng dụng, mà toàn bộ các gói cài đặt (package) đi kèm cũng có quyền y chang, khiến hệ thống trở nên mong manh hơn bao giờ hết.
Điều này nghe qua không hề hợp lý. Tại sao một công cụ lint code lại phải có toàn quyền truy cập biến môi trường và mạng của bạn?
Một số vụ việc liên quan tới Node và npm
Do Node và các gói của nó có quyền truy cập không giới hạn vào mọi thứ, và các gói lại phụ thuộc và nhiều gói khác. Nhiều sự cố đã xuất hiện khi mã độc được thêm vào các gói npm.
- Đánh cắp dữ liệu người dùng từ form
- Tấn công shell injection
- Cài mã độc vào máy
- ...
Một trong những vụ tấn công chuỗi cung ứng (Supply chain) nguy hiểm nhất gần đây liên quan tới 1 thư viện vô cùng phổ biến là axios.
- Ngày 30–31/03/2026, thư viện phổ biến axios trên npm đã bị chiếm quyền maintainer account và phát hành các phiên bản độc hại.
- Phát hành các version độc hại: axios@1.14.1, axios@0.30.4
- Tiêm nhiễm thêm dependency độc hại: plain-crypto-js@4.2.1
Khi cài npm axios thì chúng sẽ
- Kết nối tới C2 server để nhận lệnh từ xa
- Thực thi command trên máy (RCE full)
- Tải thêm payload theo OS (Windows / macOS / Linux)
- Tự xoá dấu vết sau khi thực thi để tránh bị phát hiện
2. Deno: bảo mật ngay từ mặc định
Deno được xây dựng từ đầu với bảo mật là nguyên tắc cốt lõi.
Mặc định, chương trình Deno không có quyền truy cập vào:
- Hệ thống tập tin
- Mạng
- Biến môi trường
- Chạy tiến trình con
- ...
Lập trình viên phải chủ động cấp quyền thông qua các permission flags.
Ví dụ:
deno run --allow-read app.ts
deno run --allow-net app.ts
Các permission flags này cũng áp dụng cho dependencies.
Do đó, dù thư viện cài vào có mã độc nhưng chúng đành chịu bó tay bó chân nếu không được cấp quyền thực thi
Ví dụ:
# Chỉ cho phép đọc foo.txt và bar.txt
deno run --allow-read=foo.txt,bar.txt script.ts
# Cho phép truy cập mạng nhưng chặn một domain cụ thể
deno run --allow-net --deny-net=evil.com script.ts
Deno cũng hỗ trợ:
--deny-read--deny-write--deny-net--deny-env--deny-run
Để xem chi tiết hơn và các quyền truy cập cụ thể. Có thể xem ở đây
3. Sự thay đổi đến Node.js
Đội ngũ phát triển của Node.js hoàn toàn nhận thức được những hạn chế và nguy cơ do cơ chế cấp quyền "hào phóng" đem lại. Do đó, từ phiên bản Node.js 20.0.0 đã giới thiếu cơ chế phân quyền (Permission Model) và giờ đã là một tính năng ổn định (Stable).
Process-based Permissions (Quyền hạn cấp Tiến trình)
Khi chạy node.js thêm cờ --permission sẽ hạn chế quyền. Nếu muốn quyền nào thì phải thêm cờ để cấp quyền đó.
Ví dụ
$ node --permission index.js
Error: Access to this API has been restricted
at node:internal/main/run_main_module:23:47 {
code: 'ERR_ACCESS_DENIED',
permission: 'FileSystemRead',
resource: '/home/user/index.js'
}
Cần thêm quyền đọc file với cảnh báo trên
$ node --permission --allow-read-fs=* index.js
Việc 1 lệnh chạy có thể quá dài do phải cấp nhiều quyền, có thể dùng file cấu hình node.config.json
{
"permission": {
"allow-fs-read": ["./foo"],
"allow-fs-write": ["./bar"],
"allow-child-process": true,
"allow-worker": true,
"allow-net": true,
"allow-addons": false,
"allow-ffi": false
}
}
Khi đó, lệnh sẽ được rút gọn lại như sau
$ node --experimental-default-config-file app.js
Danh sách các quyền bị cờ có thể xem ở đây
Runtime API
Khi chạy node.js với cờ ``--permissionthì đối tượng process có thêm thuộc tính mớipermission`. Có thể kiểm tra hoặc ngắt quyền với tiến trình
const fs = require('node:fs');
// Đọc cấu hình lúc khởi động khi còn quyền
const config = fs.readFileSync('/etc/myapp/config.json', 'utf8');
// bỏ quyền đọc vào /etc/myapp sau khi khởi tạo xong
process.permission.drop('fs.read', '/etc/myapp');
// Lệnh này bây giờ sẽ ném ra lỗi ERR_ACCESS_DENIED
process.permission.has('fs.read', '/etc/myapp/config.json'); // false
// bỏ hoàn toàn quyền đối với tiến trình con (child process)
process.permission.drop('child');
Những vấn đề cũng như hạn chế còn tồn tại
- Cờ
--permissionchưa phải là tính năng mặc định. Và để cho nhanh thì có lẽ nhiều nhà phát triển sẽ ko dùng nó. - Mô hình phân quyền không được kế thừa sang worker thread. Có nghĩa là các giới hạn về quyền chỉ áp dụng cho process chính. Nếu process đó tạo ra một Worker Thread, worker đó không phải tuân thủ theo.
- Mô hình phân quyền chỉ được kích hoạt sau khi Node.js đã khởi tạo xong môi trường. Do đó sẽ ko có tác dụng với một số cờ cần đọc file trước bước đó như
--env-filehay--openssl-config. - Các Run-Time Loadable Extensions không thể nạp được khi cờ permission kích hoạt, điển hình là mô-đun sqlite sẽ bị ảnh hưởng.
- Mô hình phân quyền kiểm tra quyền tại thời điểm mở file (fs.open, fs.readFile...). Nhưng nếu file descriptor (fd) đã được mở sẵn từ trước ( ví dụ do process cha truyền vào qua stdio, hoặc mở trước khi --permission kích hoạt) thì các thao tác read/write trên fd đó không bị kiểm tra lại.
- Kiểm tra quyền dựa trên đường dẫn khai báo, không dựa trên đích đến thực sự của symlink.
- Node.js tin tưởng hoàn toàn vào hệ điều hành nơi nó đang chạy. Do đó, kẻ xấu có thể lợi dụng các lời gọi ở cấp độ hệ điều hành ví dụ như process._debugProcess(pid) để chiếm quyền trái phép.
Tham khảo
All Rights Reserved