Write-up ngắn về quá trình phân tích và tái hiện CVE-2026-33937 trong Handlebars thông qua lab FreshShop. Chủ đề chính: Type Confusion → AST Injection → JavaScript Code Injection → Remote Code Execution.
1. Giới thiệu
Trong quá trình nghiên cứu các lỗ hổng liên quan đến template engine trên Node.js, mình tập trung vào một bug khá thú vị trong Handlebars: khi Handlebars.compile() không chỉ nhận template string thông thường, mà còn có thể nhận trực tiếp một AST object đã được parse sẵn.
Điều này trở nên nguy hiểm nếu ứng dụng nhận dữ liệu từ người dùng, deserialize thành object, rồi truyền thẳng object đó vào Handlebars.compile() mà không kiểm tra kiểu dữ liệu.
CVE-2026-33937 khai thác chính điểm này.
Nói ngắn gọn:
User input
↓
JSON.parse()
↓
AST object
↓
Handlebars.compile(AST)
↓
Generated JavaScript
↓
new Function()
↓
Remote Code Execution
2. Tổng quan CVE-2026-33937
CVE-2026-33937 ảnh hưởng đến package handlebars trên npm, trong các phiên bản từ 4.0.0 đến 4.7.8.
Thông tin chính:
CVE ID: CVE-2026-33937
Package: handlebars
Affected versions: >= 4.0.0 <= 4.7.8
Patched version: 4.7.9
Severity: Critical
CWE: CWE-843 Type Confusion, CWE-94 Code Injection
Về bản chất, đây là lỗi Type Confusion trong quá trình xử lý AST. Một field đáng lẽ phải là number lại có thể bị attacker thay bằng JavaScript code string. Khi compiler sinh source code, đoạn string này đi vào generated JavaScript và được thực thi.
3. FreshShop Lab
Để tái hiện lỗ hổng, mình dựng một lab web tên FreshShop. Đây là một ứng dụng giả lập trang bán hàng có chức năng tài khoản người dùng, chỉnh sửa profile và hiển thị phần mô tả cá nhân.
Attack surface chính nằm ở phần About You trong trang edit profile.
Người dùng có thể nhập nội dung vào trường này, lưu lại, sau đó nội dung được render ở trang profile.

Ban đầu, trường About You trông giống một input bình thường. Tuy nhiên, khi nhập payload template đơn giản như:
{{this}}
và truy cập lại trang profile, ứng dụng trả về:
[object Object]
Kết quả này cho thấy nội dung người dùng nhập vào đang được xử lý bởi template engine, thay vì chỉ hiển thị raw text.

4. Root cause
Root cause nằm ở việc Handlebars.compile() có thể nhận AST object trực tiếp.
Trong flow bình thường, template string sẽ được parse thành AST nội bộ. Nhưng nếu input đã là một object có dạng Program, Handlebars có thể bỏ qua bước parse và dùng trực tiếp AST đó.
Vấn đề nằm ở node NumberLiteral.
Về mặt logic, node này phải có value là number:
{
"type": "NumberLiteral",
"value": 1
}
Nhưng trong phiên bản vulnerable, compiler không kiểm tra chặt kiểu dữ liệu của NumberLiteral.value. Vì vậy attacker có thể biến value thành một chuỗi JavaScript.
Luồng lỗi có thể tóm tắt như sau:
NumberLiteral.value
↓ không validate typeof
pushLiteral()
↓
Generated JavaScript source
↓
new Function(source)
↓
JavaScript code execution
Điểm quan trọng là đây không phải lỗi template injection kiểu thông thường. Lỗ hổng này xảy ra sâu hơn, ở tầng AST compiler và JavaScript code generation.
5. Luồng khai thác trong lab
Trong lab FreshShop, attacker không chỉ gửi text bình thường vào trường About You. Thay vào đó, attacker gửi một AST object được craft sẵn.
AST này vẫn có cấu trúc hợp lệ theo Handlebars, nhưng phần NumberLiteral.value bị thay bằng JavaScript expression để thoát khỏi context ban đầu và gọi API của Node.js.
Flow khai thác:
Attacker nhập crafted AST vào About You
↓
Server lưu payload
↓
Profile page render lại nội dung
↓
Payload được đưa vào Handlebars.compile()
↓
Compiler sinh JavaScript source
↓
new Function() compile source
↓
Payload chạy trong Node.js process
Trong quá trình kiểm thử, payload được gửi qua Burp để quan sát request/response rõ ràng hơn.

6. Xác nhận RCE
Sau khi payload được lưu và trang profile được render lại, nội dung phần About không còn là text thông thường nữa. Server trả về output từ command chạy trong môi trường Node.js.
Trong PoC, output chứng minh server đã đọc được thông tin từ filesystem, ví dụ /etc/passwd.

Điều này xác nhận chain đã hoàn chỉnh:
User-controlled input
→ AST injection
→ Handlebars compiler
→ Generated JavaScript
→ Node.js execution
→ Command output returned
Ở mức bảo mật, đây là Remote Code Execution vì attacker đã chuyển từ việc kiểm soát nội dung profile sang thực thi code trên server.
7. Patch analysis
Ở phiên bản 4.7.9, Handlebars bổ sung bước validate AST trước khi compile.
Thay vì tin tưởng trực tiếp AST object được truyền vào, bản vá kiểm tra kiểu dữ liệu của các node quan trọng. Với NumberLiteral, value bắt buộc phải là number hợp lệ.
Logic patch có thể hiểu như sau:
Nếu node.type === "NumberLiteral"
↓
Kiểm tra typeof node.value === "number"
↓
Nếu không phải number → throw exception
↓
Payload bị chặn trước khi vào compiler pipeline
Khi chạy lại cùng payload trên phiên bản đã vá, ứng dụng trả về lỗi validate thay vì thực thi payload.

Điểm đáng chú ý là payload bị chặn ngay từ entry point của AST validation, nên không còn cơ hội đi tới pushLiteral() hoặc new Function().
8. Security impact
CVE-2026-33937 có impact nghiêm trọng vì attacker có thể đạt được RCE trên server Node.js nếu ứng dụng thỏa các điều kiện sau:
Ứng dụng dùng handlebars <= 4.7.8
Ứng dụng nhận input do user kiểm soát
Input được deserialize thành object
Object được truyền vào Handlebars.compile()
Không validate typeof input === "string"
Trong kịch bản thực tế, attacker có thể:
- Đọc file nhạy cảm trong container/server.
- Truy xuất environment variables.
- Thu thập thông tin cấu hình ứng dụng.
- Gọi command hệ thống.
- Pivot sang service nội bộ nếu server có quyền truy cập.
- Gây ảnh hưởng đến confidentiality, integrity và availability.
Nếu ứng dụng chạy bằng quyền cao, ví dụ root trong Docker container, impact sẽ còn nghiêm trọng hơn.
9. Kết luận
CVE-2026-33937 là một ví dụ hay về lỗi xảy ra ở ranh giới giữa template rendering và compiler internals.
Bài học chính:
Không truyền object không tin cậy vào Handlebars.compile()
Không cho user kiểm soát AST
Luôn validate input type trước khi compile/render
Upgrade handlebars lên 4.7.9 hoặc mới hơn
Giới hạn quyền runtime của Node.js process
Điểm nguy hiểm nhất của bug này là ứng dụng có thể trông rất bình thường: chỉ là một form profile cho người dùng nhập mô tả cá nhân. Nhưng nếu dữ liệu đó đi vào template compiler không an toàn, attacker có thể biến một input text thành RCE.
Đây cũng là lý do các hàm dạng compile, render, eval, Function, hoặc bất kỳ cơ chế code generation nào luôn cần được xem là high-risk sink khi audit bảo mật.