توضیح سادهی باگ CVE-2025-55182 در React / Next.js
این متن برای کسی نوشته شده که شاید فقط کمی جاوااسکریپت بداند یا حتی فقط بداند «وبسایت چیه».
حرفهای پیچیده را تا حد ممکن تبدیل میکنیم به مثالهای واقعی و ساده.
۱. خلاصهی ماجرا در یک پاراگراف
در بعضی نسخههای React و Next.js، وقتی کاربر از سمت مرورگر با «Server Actions / Server Functions» صحبت میکرد،
سمت سرور اطلاعات را از حالت رشتهای به آبجکت تبدیل میکرد (deserialize).
در این تبدیل، یک اشتباه امنیتی وجود داشت که باعث میشد یک هکر بتواند:
- به چیزهای داخلی جاوااسکریپت دسترسی بگیرد (مثل
Functionconstructor) - آن را طوری استفاده کند که روی سرور شما کد دلخواه اجرا شود (Remote Code Execution یا RCE)
یعنی اگر وبسایت شما در معرض اینترنت است و این باگ را داشته،
هکر میتوانسته روی سرور شما دستورهایی مثل این اجرا کند:
require('child_process').exec('rm -rf /');
۲. اول از همه: Server Function یعنی چی؟
مثال دنیای واقعی
فرض کن:
- تو توی خانه نشستی (مرورگر کاربر)
- یک دوستت توی اداره پشت سرور نشسته (سرور)
- تو به دوستت میگی: «لطفا برو از بانک استعلام بگیر»
چون خودت به بانک دسترسی مستقیم نداری، ولی او دارد.
در Next.js / React:
- تو در سمت سرور یک تابع داری مثلا: 'use server' export async function createUser(data) { // کار با دیتابیس }
- از سمت کلاینت، این تابع را صدا میزنی،
ولی در حقیقت یک درخواست HTTP به سرور میرود و دادهها با یک فرمت خاص فرستاده میشوند.
این فرمت خاص اسمش هست React Flight Protocol.
همان «زبانِ صحبت» بین مرورگر و سرور برای React Server Components / Actions.
۳. React Flight را خیلی ساده تصور کنیم
React دادهها را «تکهتکه» میفرستد؛ هر تکه را میتوانی مثل یک برگهی شمارهدار داخل یک پوشهی بایگانی تصور کنی.
مثال:
files = {
"0": (None, '["$1"]'),
"1": (None, '{"object":"fruit","name":"$2:fruitName"}'),
"2": (None, '{"fruitName":"cherry"}'),
}
این یعنی:
- برگه ۰ میگوید: برو برگه ۱ را نگاه کن (
"$1") - برگه ۱ میگوید: من یک شیء هستم با:
object = "fruit"name = مقدار فیلد fruitName در برگه ۲
- برگه ۲ میگوید:
fruitName = "cherry"
وقتی سرور همهی این برگهها را کنار هم میگذارد، نتیجه میشود:
{ object: "fruit", name: "cherry" }
پس ایده این است:
- هر برگه میتواند به برگههای دیگر ارجاع بدهد.
- سرور باید این ارجاعات را دنبال کند و در آخر یک آبجکت نهایی بسازد.
تا اینجای کار مشکلی نیست.
۴. مشکل از کجا شروع شد؟ (داستان prototype)
در جاوااسکریپت، هر شیء یک «پدر و مادر مخفی» دارد که به آن prototype میگویند.
مثال دنیای واقعی
فرض کن:
- هر کارمند در یک شرکت، یک پوشهی شخصی دارد (همان object)
- اما یک پوشهی عمومی هم وجود دارد (prototype) که در آن:
- قوانین کلی شرکت
- فرمهای عمومی
- دستورالعملها
هستند.
وقتی میپرسی:
در پوشهی «علی» فرم "مرخصی" هست؟
سیستم میگوید:
- اگر در پوشهی علی نبود، برو در پوشهی عمومی (prototype) بگرد.
در جاوااسکریپت:
obj.__proto__
به همین پوشهی عمومی وصل است.
حالا مشکل React این بود:
وقتی داشت از روی برگهها (chunks) دادهها را میخواند،
چک نمیکرد که key واقعا روی خود آبجکت هست یا از prototype میآید.
یعنی اگر هکر میگفت:
"$1:__proto__:constructor:constructor"
React میرفت:
- برگهی ۱
- روی
__proto__ - روی
constructor - دوباره روی
constructor
و در نهایت میرسید به تابع سراسری Function.
این دقیقا معادل این بود که به مهمان اجازه بدهی نهتنها به کشوی شخصی خودش،
بلکه به کمد کلیدهای اصلی ساختمان هم دسترسی داشته باشد.
۵. Function constructor چرا خطرناک است؟
در جاوااسکریپت:
const f = Function("console.log('سلام از سرور')");
f(); // این کد اجرا میشود
این یعنی:
- یک رشته (string) کد میگیری
- آن را تبدیل به یک تابع میکنی
- بعد تابع را اجرا میکنی
اگر هکر بتواند:
- به خود
Functionبرسد - یک رشتهی دلخواه هم به آن بدهد
- و بعد حاصل تابع را اجرا کند
در واقع میتواند هر کدی روی سرور اجرا کند.
۶. چطوری هکرها از داخل Flight به اجرای تابع رسیدند؟
قدم ۱: رسیدن به Function در دادهها
در PoC این مثال آمده:
files = {
"0": (None, '["$1:__proto__:constructor:constructor"]'),
"1": (None, '{"x":1}'),
}
معنیاش:
- از برگه ۰، مسیر
"$1:__proto__:constructor:constructor"را دنبال کن - این در نهایت میشود
Function
نتیجه این است که در فرایند تبدیل، سرور در یک متغیر داخلی خودش، به Function دسترسی پیدا میکند.
قدم ۲: وادار کردن سیستم به «صدا زدن» این تابع
در Next.js، جایی وجود دارد که این کار را میکند:
const result = await decodeReplyFromBusboy(...);
اگر result یک آبجکت باشد که then دارد،
جاوااسکریپت به صورت خودکار آن را به عنوان شبیه Promise میبیند و این کار را میکند:
result.then(resolve, reject);
پس اگر هکر بتواند then را برابر با Function(...) قرار دهد،
ماجرا این طوری میشود:
thenمیشود همان تابعی که ازFunction("کد")آمدهawaitباعث اجرای آن میشود- کد داخلش روی سرور اجرا میشود
یک payload ساده برای تست این بود:
files = {
"0": (None, '{"then":"$1:__proto__:constructor:constructor"}'),
"1": (None, '{"x":1}'),
}
اینجا:
thenبرابرFunctionمیشود- بعد وقتی
awaitروی آن صدا میشود، چیزهای عجیبی رخ میدهد و خطایSyntaxErrorدیده میشود
این مرحله بیشتر برای اثبات این است که:
ما توانستیم
thenرا دستکاری کنیم تا بهFunctionاشاره کند و سیستم تلاش کرد آن را اجرا کند.
۷. ترفند اصلی PoC: ساختن «chunk جعلی» مثل یک فرم پر از دروغ
PoC یک حرکت هوشمندانه انجام میدهد:
- یک chunk میسازد که ظاهرش شبیه chunk داخلی React است
- خودش را چند بار به خودش ارجاع میدهد
- کاری میکند که داخل کد React، این chunk جعلی به عنوان دادهی معتبر دیده شود
مثال سادهی دنیای واقعی
فرض کن:
- شرکتت برای همهی درخواستها فرم دارد
- کارمندها فرمها را بررسی و اجرا میکنند
- یک نفر یک فرم قلابی و خیلی شبیه فرم واقعی میسازد
- این فرم را طوری پر میکند که:
- بخش «نوع درخواست» بگوید: «دوباره این فرم را بخوان»
- بخش «مقصد» بگوید: «از کمد کلید یک کپی از کلید اصلی بردار»
- بخش «دستورالعمل» بگوید: «با این کلید برو گاوصندوق را باز کن و پولها را بفرست فلانجا»
اگر سیستم هیچ چکی روی اینکه «این فرم واقعا از خود شرکت است یا از بیرون» نکند،
کارمند سادهدل دقیقا همان کار را انجام میدهد.
در PoC:
- از یک قابلیت خاص با پیشوند
"$@"استفاده میشود که میگوید:
«به من خود chunk خام را بده نه مقدار نهاییاش» - بعد با چند دور بازی با
thenوstatusوvalueکاری میکنند که:- تابع داخلی
initializeModelChunkروی این دادهی جعلی صدا زده شود - داخل آن، JSON parse شود
- و در نهایت
Function("کد هکر")ساخته و اجرا شود
- تابع داخلی
۸. یک تکهی مهم از زنجیرهی RCE
در یک بخش از کد React، چیزی شبیه این وجود داشت:
case "B":
return (
(obj = parseInt(value.slice(2, 16))),
response._formData.get(response._prefix + obj)
);
PoC از این بخش اینطور سوءاستفاده میکند:
response._formData.getرا برابرFunctionمیگذاردresponse._prefixرا میگذارد برابر رشتهی کد دلخواه
در نهایت، چیزی که اجرا میشود این است:
Function("process.mainModule.require('child_process').execSync('calc');" + "0");
این Function(...) میشود همان then و چون در زنجیرهی await قرار دارد، اجرا میشود.
این یعنی هکر با استفاده از چند ارجاع تو در تو، سیستم را مجبور میکند کدی که خودش نوشته
به عنوان تابع در سرور ساخته و سپس اجرا شود.
۹. چرا این باگ خیلی خطرناک است؟
چند نکتهی مهم:
- قبل از چک کردن مجوزها رخ میدهد
یعنی حتی قبل از اینکه سرور بررسی کند «این کاربر اجازهی این اکشن را دارد یا نه»،
کد Flight parse میشود و exploit میتواند اجرا شود. - فقط نیاز به دسترسی به endpoint دارد
هر کسی که بتواند یک درخواست شبیه فرم ارسال کند به endpoint Server Actions،
میتواند این payload را بفرستد. - بدون نیاز به کدنویسی عجیب در پروژهی شما
حتی اگر خود شما Server Action خاصی ننوشته باشید،
ممکن است فقط به خاطر استفاده از App Router / RSC در Next.js، این مسیر باز باشد.
در دنیای واقعی، این شبیه این است که:
هر کسی بتواند فقط با فرستادن یک فرم ظاهرا عادی،
نگهبان ساختمان را قانع کند درِ اتاق سرور را باز کند و دستورهایی روی سرور اجرا کند.
۱۰. پچ چطوری درستش کرد؟
در بخشی از کد که ماژولها را بر اساس اسمشان برمیگرداند، قبلا اینطوری بود:
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
return moduleExports[metadata[NAME]];
}
پچ آن را به این تبدیل کرده:
export function requireModule<T>(metadata: ClientReference<T>): T {
const moduleExports = parcelRequire(metadata[ID]);
if (hasOwnProperty.call(moduleExports, metadata[NAME])) {
return moduleExports[metadata[NAME]];
}
return (undefined: any);
}
اینجا مهمترین نکته این است:
- قبلا هر چیزی در زنجیرهی prototype (حتی از prototype) قابل دسترسی بود
- الان فقط اگر خود آبجکت آن property را داشته باشد اجازه میدهد
یعنی دیگر نمیتوانی با __proto__ و constructor به چیزهای خطرناک داخلی برسی.
۱۱. اگر برنامهنویس Next.js / React هستم، چه کار باید بکنم؟
به شکل خیلی ساده:
- نسخهها را آپدیت کن
- پکیجهای React server (مثل
react-server-dom-webpackو مشابه) - Next.js را به نسخهای که تیم رسمی گفته امن است، ارتقا بده
- اگر الان نمیتوانی آپدیت کنی (فقط موقت)
- endpointهایی که Server Actions را هندل میکنند را پشت:
- فایروال
- IP allowlist
- یا هر راهی که فقط به کاربران مورد اعتماد دسترسی بدهد
قرار بده
- این فقط یک مسکن موقت است، نه درمان اصلی
- بعد از آپدیت، حتما deployment کامل انجام بده
فقط تغییر در
package.jsonکافی نیست.
باید build جدید بگیری و سرور را با نسخهی جدید بالا بیاوری.
۱۲. جمعبندی خیلی کوتاه
- React / Next.js برای صحبت بین مرورگر و سرور از یک فرمت تکهتکه (Flight) استفاده میکند.
- در نسخههای مشکلدار، سیستم موقع سر هم کردن این تکهها:
- اجازه میداد به prototype objectها دسترسی بگیری
- از آنجا به
Functionبرسی
- با چند حقه، هکر میتوانست:
Function("کد خودم")بسازد- و آن را در زنجیرهی
awaitطوری جا بدهد که روی سرور اجرا شود
- این یعنی Remote Code Execution بدون نیاز به لاگین.
- پچ با محدود کردن دسترسی به propertyهای خود آبجکت (و نه prototype) این راه را بست.
- راهحل عملی برای ما: آپدیت سریع dependencyها و deployment نسخهی جدید.