CSRF چیست و چگونه از آن جلوگیری کنیم؟
چرا حمله CSRF (جعل درخواست بینسایتی) بدون اینکه کاربر متوجه شود، از حساب بانکی یا پنل ادمین سوءاستفاده میکند؟ راهنمای عملی از مکانیزم تا روشهای دفاع لایهای در وردپرس و فریمورکهای وب.
چند سال پیش، در یک مشاوره امنیتی برای یک فروشگاه اینترنتی کوچک، گزارش عجیبی رسید: چند سفارش ناشناس در سیستم ثبت شده بود، ولی هیچکدام از مشتریان نمیگفتند که سفارشی دادهاند. بعد از چند روز بررسی، مشخص شد حملهای بهنام CSRF (Cross-Site Request Forgery) اتفاق افتاده؛ مهاجم، با فریبدادن یک کاربر لاگینشده به کلیک روی یک لینک مخفی، درخواست ثبت سفارش را از طرف او فرستاده بود. آن پروژه، در فرانتاند جذاب بود، ولی در بکاند، توکن محافظت از CSRF وجود نداشت. تجربهام میگوید این حمله، برخلاف XSS یا SQL Injection، بیشتر از جنس بیتوجهی معماری میآید تا جنس کد؛ به همین دلیل، در پروژههای زیادی پیدا میشود که در بقیه لایهها امن هستند. در این نوشته، همان مسیر شناخت و دفاع را میگویم که در پروژههای واقعی طی میکنم.
CSRF چیست؟ تعریف دقیق و یک مثال واقعی
CSRF (Cross-Site Request Forgery) یا «جعل درخواست بینسایتی» یک کلاس حمله است که در آن، مهاجم کاربر لاگینشده در سایت قربانی را متقاعد میکند تا درخواستی را علیه خودش ارسال کند، بدون آنکه خود کاربر متوجه شود. جمله کلیدی در این تعریف، «کاربر لاگینشده» است؛ چون CSRF از کوکی نشست کاربر استفاده میکند و اگر کاربر وارد سایت نشده باشد، حمله بیاثر است. این نقطه، تفاوت بنیادی CSRF با حملاتی مثل XSS است که به اعتماد مرورگر به محتوای صفحه وابستهاند، نه به هویت کاربر.
مثال واقعی که در همان فروشگاه دیدم، ساده بود: مهاجم یک فرم مخفی در سایتی دیگر ساخت که فیلدهایش را از پیش پر کرده بود و کاربر را با یک لینک کوتاه به همان صفحه کشیده بود. کاربر که در فروشگاه لاگین بود، ناخواسته فرم را ارسال کرد و مرورگر، کوکی نشست فروشگاه را هم با درخواست فرستاد. نتیجه، یک سفارش با محصولی گرانقیمت به نام کاربر بود. اگر میخواهید با مفهوم کلی آسیبپذیریها آشنا شوید، آسیبپذیری وب چیست نقطه شروع خوبی است و انواع رایج آسیبپذیریها زمینه کلیتری میدهد.
در حمله CSRF، مهاجم رمز عبور را نمیدزدد؛ فقط از مرورگر شما بهعنوان دستیار، برای ارسال یک درخواست استفاده میکند.
چرا CSRF خطرناک است؟
دو ویژگی CSRF، آن را از بسیاری از حملات دیگر متمایز میکند. اول، کاربر هیچ سیگنالی از حمله نمیبیند؛ هیچ خطای ورود، هیچ اعلان، هیچ صفحه عجیبی. دوم، درخواستهایی که مهاجم میتواند بفرستد، میتوانند شامل عملیات حساس باشند: تغییر رمز عبور، حذف حساب، انتقال وجه، ثبت سفارش، تغییر ایمیل بازیابی. سه دسته از سایتها بیشتر از همه در معرض این حمله هستند:
- سایتهای بانکی و پرداخت: بیشترین آسیب مالی؛ حتی یک انتقال کوچک هم برای کاربر فاجعه است.
- پنلهای مدیریت و داشبوردها: مهاجم میتواند تغییر تنظیمات یا افزودن کاربر جدید را از طرف ادمین انجام دهد.
- فروشگاههای اینترنتی: در تجربه من، موارد ثبت سفارش ناخواسته، شایعترین نشانه CSRF در سایتهای تجاری است.
نکتهای که در پروژههای واقعی زیاد دیدهام: خیلی از توسعهدهندگان، چون CSRF را در فهرست «حملات پیچیده» طبقهبندی میکنند، آن را به بعد موکول میکنند، در حالی که این حمله در برابر سایتهای بدون دفاع، جزو سادهترین حملات برای اجراست. تنها چیزی که برای اجرا نیاز دارد، یک کاربر لاگینشده و یک صفحه مخفی، که هر دو در دسترس مهاجم است.
مکانیزم حمله گامبهگام
درک دقیق مکانیزم، کلید دفاع درست است. یک حمله CSRF معمولاً در پنج گام انجام میشود:
- کاربر در سایت شما وارد میشود و کوکی نشست در مرورگرش ذخیره میشود.
- مهاجم صفحهای میسازد که یک درخواست مخفی به سایت شما میفرستد (فرم پنهان یا تصویر با
srcمخرب). - کاربر با یک لینک فریبنده، وارد صفحه مهاجم میشود.
- مرورگر، درخواست را به سایت شما میفرستد و کوکی نشست را هم همراهش میگذارد.
- سایت شما، چون کوکی معتبر است، درخواست را از طرف کاربر معتبر میداند و آن را اجرا میکند.
نمونه کد صفحه مهاجم، برای درک بهتر:
<form action="https://victim-bank.com/transfer" method="POST" id="x">
<input type="hidden" name="to" value="attacker-account" />
<input type="hidden" name="amount" value="1000" />
</form>
<script>document.getElementById('x').submit();</script>
نکته کلیدی این است که مرورگر، بدون پرسیدن از کاربر، کوکیها را با هر درخواست به همان دامنه میفرستد. اگر سایت شما، آن درخواست را با توکن CSRF تأیید نکند، هیچ مانعی بین فرم مهاجم و اجرای عملیات وجود ندارد. همین نکته، پایه همه روشهای دفاع است که در ادامه میگویم.
CSRF و XSS: تفاوت را اشتباه نگیرید
در جلسههای امنیتی، یکی از پرتکرارترین اشتباهات این است که CSRF با XSS اشتباه گرفته میشود. هر دو، از جنس اعتماد سوءاستفاده میکنند، ولی مسیر و دفاعشان متفاوت است:
| معیار | CSRF | XSS |
|---|---|---|
| هدف حمله | اعتماد سایت به مرورگر کاربر | اعتماد مرورگر به محتوای صفحه |
| وضعیت کاربر | باید لاگین باشد | لازم نیست لاگین باشد |
| درخواست از کجا | از دامنه مهاجم | از همان دامنه آسیبدیده |
| نقطه دفاع اصلی | توکن CSRF و SameSite | Escape خروجی و CSP |
| نشانه در سایت | عملیات ناخواسته در پنل | اسکریپت اجراشده در صفحه |
نکتهای که در پروژهها زیاد دیدهام: XSS میتواند به CSRF تبدیل شود. اگر سایت شما در برابر XSS آسیبپذیر باشد، مهاجم میتواند اسکریپت مخرب را در همان دامنه شما اجرا کند و درخواستهای آگاهانه بفرستد. یعنی CSRF، پشت XSS مینشیند و دفاع لایهای را سختتر میکند. به همین دلیل، در معماری دفاعی، هیچوقت یکی را جایگزین دیگری نمیبینم. اگر میخواهید بدانید چطور XSS دفاع میشود، حملات XSS و راههای مقابله را ببینید. برای SQL Injection هم که در دسته حملههای مشابه قرار میگیرد، SQL Injection چیست و چگونه جلوگیری کنیم راهنمای مکمل است.
روشهای دفاع: از توکن تا SameSite
دفاع در برابر CSRF، لایهای است، نه تکروشی. در تجربه من، ترکیب سه لایه زیر، تقریباً تمام موارد CSRF را میپوشاند:
لایه اول: توکن CSRF (Synchronizer Token)
روش استاندارد و پیشنهاد اول. در هر فرم یا درخواست تغییردهنده، یک توکن یکتا همراه کاربر فرستاده میشود و سرور، قبل از اجرا، معتبر بودن آن را بررسی میکند. مهاجم، به توکن دسترسی ندارد، چون در صفحه مهاجم تولید نمیشود و از دامنه شما هم نمیتواند بخواند (بهدلیل Same-Origin Policy).
<form method="POST" action="/transfer">
<input type="hidden" name="csrf_token" value="TOKEN_HERE">
...
</form>
لایه دوم: SameSite Cookie
ویژگی SameSite روی کوکی، به مرورگر میگوید آیا این کوکی را با درخواستهای بینسایتی بفرستد یا نه. مقدار Lax در اکثر موارد کافی است و Strict امنتر ولی برخی مواقع روی تجربه کاربری اثر میگذارد (مثلاً وقتی کاربر از لینک ایمیل وارد سایت میشود). در تجربه من، Lax پیشفرض منطقی است و برای صفحات حساس، Strict.
Set-Cookie: session=abc; SameSite=Lax; Secure; HttpOnly
لایه سوم: بررسی Origin و Referer
سرور میتواند هدرهای Origin و Referer را بررسی کند و درخواستهایی که از دامنههای غیرمجاز میآیند، رد کند. این لایه بهتنهایی کافی نیست (چون برخی مرورگرها یا پراکسیها این هدرها را حذف میکنند)، اما بهعنوان لایه مکمل، ارزش دارد.
هر لایهای که تنها استفاده شود، یک شکاف باقی میگذارد؛ دفاع لایهای، تنها راه بستن شکافهای باقیمانده است.
روشهای مکمل
- توکن دوگانه (Double Submit Cookie): توکن در کوکی و در پارامتر درخواست ارسال میشود و سرور، همخوانی این دو را بررسی میکند. مناسب برای APIهای stateless.
- هدر سفارشی (Custom Header): اگر درخواست از JavaScript ارسال میشود، الزام وجود هدر
X-Requested-Withیا مشابه، چون مهاجم نمیتواند این هدر را از دامنه خود بسازد. - تأیید دوباره برای عملیات حساس: برای کارهایی مثل تغییر رمز عبور یا انتقال وجه، دوباره رمز عبور یا 2FA را از کاربر بپرسید. این تنها لایهای است که حتی در صورت نفوذ، از عملیات حساس جلوگیری میکند. جزئیات در تأثیر 2FA بر امنیت آمده است.
محافظت CSRF در فریمورکهای محبوب
خبر خوب اینکه فریمورکهای مدرن، محافظت CSRF را بهصورت بومی ارائه میدهند. در وردپرس، برای فرمهای پنل ادمین، تابع wp_nonce_field و برای بررسی، check_admin_referer استفاده میشود. نمونه ساده:
<?php
// در فرم
wp_nonce_field('my_action_name', 'my_nonce');
// در پردازش
if (! isset($_POST['my_nonce']) || ! wp_verify_nonce($_POST['my_nonce'], 'my_action_name')) {
wp_die('خطای امنیتی');
}
در Django، middleware بومی CsrfViewMiddleware بهطور پیشفرض فعال است و فقط کافی است در قالبها {% csrf_token %} را اضافه کنید. امنیت Django و لایههای دفاعی آن بهطور کامل در امنیت در Django: بهترین روشها آمده است. در Laravel، توکن CSRF خودکار در هر فرم با @csrf تولید میشود و در Express.js نیاز به middleware اضافه مثل csurf یا جایگزینهای مدرن آن است. نکته مشترک همه: هیچکدام، اگر از مسیر خودکار خارج شوید، از شما محافظت نمیکنند. اگر از فرمهای سفارشی یا APIهای جداگانه استفاده میکنید، خودتان مسئول پیادهسازی این لایه هستید.
در وردپرس، علاوه بر فرمهای ادمین، افزونهها و قالبهایی که فرم سفارشی دارند هم باید از wp_nonce استفاده کنند. اگر افزونهای دیدید که فرم POST دارد و تابع nonce در آن نیست، آن افزونه، یک حفره CSRF جدی است. راهنمای بیشتر در راهنمای امنیت وردپرس برای مبتدیان آمده است.
اشتباهات رایج در پیادهسازی CSRF
در پروژههای واقعی، پنج اشتباه را بیش از بقیه دیدهام:
- خاموش کردن محافظت در توسعه و فراموش کردن در production: توسعهدهنده برای راحتی تست، محافظت CSRF را موقتاً خاموش میکند و آن را فراموش میکند. تا وقتی که سایت بهطور تصادفی به هکر آسیب نزند، کسی متوجه نمیشود.
- اعمال محافظت فقط روی فرمهای مهم: مهاجم برای رسیدن به هدف، مسیرهای جانبی را امتحان میکند. محافظت باید روی همه درخواستهای تغییردهنده باشد (POST، PUT، DELETE)، نه فقط روی چند فرم خاص.
- استفاده از توکنهای ثابت و قابلحدس: بعضیها یک توکن ثابت برای همه کاربران تولید میکنند که با یک بار خواندن، حدسزدنی است. توکن CSRF باید برای هر کاربر و بهتر، برای هر نشست منحصربهفرد باشد.
- قرار دادن توکن در URL: توکن CSRF در پارامترهای URL، در لاگهای سرور، در Referer و در تاریخچه مرورگر ثبت میشود. توکن باید در بدنه POST یا هدر سفارشی باشد، نه URL.
- بیتوجهی به APIهای stateless: در APIهایی که از JWT استفاده میکنند، CSRF بهطور طبیعی وجود ندارد چون کوکی نیست. اما اگر همان API، کوکی هم میپذیرد، حمله برمیگردد. ابزارهای احراز هویت مختلف در بهترین روشهای احراز هویت کاربران مقایسه شده و نقش کوکی در امنسازی نشستهای کاربری آمده است.
آزمون CSRF: چطور پروژه خود را بسنجیم
تست CSRF، برخلاف XSS یا SQL Injection، بیشتر از جنس تحلیل فرمهاست تا اسکن خودکار. سه روش که در پروژهها استفاده میکنم:
- بازرسی خروجی فرمها: هر فرم POST در سایت را باز کنید و در View Source، حضور توکن CSRF را بررسی کنید. اگر فرمی بدون توکن بود و عملیات مهمی انجام میداد، مشکوک است.
- ارسال درخواست بدون توکن: با ابزارهایی مثل Postman یا curl، همان درخواست را بدون توکن CSRF ارسال کنید. اگر سرور آن را پذیرفت، لایه محافظت وجود ندارد یا غیرفعال است.
- ارسال از دامنه متفاوت: یک صفحه HTML ساده روی یک دامنه دیگر بسازید که همان فرم را ارسال میکند؛ اگر درخواست انجام شد، حمله CSRF ممکن است.
روش گامبهگام تست امنیتی سایت در تست امنیت وبسایت و ابزارهای اسکن در اسکنرهای آسیبپذیری وب آمده است. توصیه من: تست CSRF را به فهرست استاندارد تست امنیتی هر پروژه اضافه کنید، چون در تجربه من، این مورد بیشتر از بقیه فراموش میشود.
جدول مرجع
| روش دفاع | پوشش | پیچیدگی پیادهسازی | نکته کلیدی |
|---|---|---|---|
| توکن CSRF | بالا | کم (با فریمورک) | استاندارد، برای همه فرمها |
| SameSite Cookie | بالا | بسیار کم | Lax پیشفرض، Strict برای حساس |
| بررسی Origin | متوسط | کم | لایه مکمل، نه تنها |
| Double Submit | بالا | متوسط | مناسب APIهای stateless |
| هدر سفارشی | متوسط | کم | فقط برای درخواستهای JS |
| تأیید دوباره حساس | بسیار بالا | متوسط | برای عملیات بحرانی |
پرسشهای کوتاه
آیا SameSite بهتنهایی کافی است؟ برای اکثر سایتها، لایه SameSite بیشترین پوشش را میدهد، اما نه کافی. ترکیب SameSite با توکن CSRF، استاندارد امروز است. اگر میخواهید روی پروژه جدی سرمایهگذاری کنید، هر دو را داشته باشید.
آیا سایتهای راستبهچپ هم در معرض CSRF هستند؟ بله، RTL و LTR هیچ ربطی به CSRF ندارند؛ چون حمله در مرورگر کاربر اجرا میشود، نه در محتوای صفحه. تفاوتهای سایتهای فارسی از جنس تایپوگرافی و چیدمان است، نه امنیت پایه.
آیا افزونههای امنیتی وردپرس، CSRF را پوشش میدهند؟ بعضی افزونهها مثل Wordfence و Sucuri روی فرمهای پنل ادمین لایه محافظت اضافه میکنند، اما فرمهای سفارشی افزونهها و قالبها را پوشش نمیدهند. مسئولیت نهایی، روی سازنده افزونه است. مقایسه افزونههای امنیتی در بهترین افزونههای امنیتی وردپرس آمده است.
اگر پروژهام API دارد و کوکی استفاده نمیکند، چه؟ اگر فقط از JWT در هدر Authorization استفاده میکنید، CSRF بهطور طبیعی وجود ندارد. اما اگر در کنار JWT، کوکی نشست هم میپذیرید، باید همان محافظت CSRF را روی کوکی اعمال کنید. جزئیات تفاوت JWT و کوکی در JWT چیست و چه کاربردی در احراز هویت دارد آمده است.
چقدر طول میکشد تا سایت را در برابر CSRF ایمن کنم؟ در تجربه من، برای یک سایت وردپرسی معمولی، بین نیم روز تا یک روز کاری؛ اگر همه فرمهای سفارشی را مرور کنید. برای یک اپلیکیشن اختصاصی، بسته به تعداد مسیرهای تغییردهنده، بین یک تا سه روز.
آن لایهای که کمتر دیده میشود
برای توسعهدهندگانی که با معماری امنیتی سر و کار دارند، CSRF یک درس بزرگتر از خودش دارد: مرز بین «هویت کاربر» و «قصد کاربر» را باید در طراحی سیستم از هم جدا کرد. کوکی نشست، فقط هویت کاربر را ثابت میکند؛ اینکه کاربر واقعاً قصد انجام این عملیات را داشته یا نه، نیازمند یک لایه اضافه است — همان لایهای که توکن CSRF، تأیید دوباره یا اثرانگشتهای رفتاری آن را میسازند. این تفکیک، در معماریهای مدرن با مفاهیمی مثل «توکنهای یکبارمصرف»، «امضای درخواست» و «Nonce» پیاده میشود و در سطح پایینتر، به همان اصل قدیمی برمیگردد که در طراحی پروتکلهای رمزنگاری هم رعایت میشود: «هیچ درخواستی، بدون تأیید صریح قصد، نباید اجرا شود.» تجربه من میگوید تیمهایی که این تفکیک را در لایههای بنیادی سیستم رعایت میکنند، نهتنها در برابر CSRF، بلکه در برابر دسته بزرگی از حملات بازپخش (replay attacks) و جعل درخواست مقاومتر میشوند. نقطه شروع عملی، ساده است: در هر endpoint که عملیات تغییردهنده انجام میدهد، از خود بپرسید «چه چیزی در این درخواست ثابت میکند که کاربر قصد انجامش را داشته؟» اگر پاسخ «فقط کوکی نشست» بود، آن endpoint یک جای خالی امنیتی دارد. اگر پاسخ شامل توکن، امضا یا تأیید دوباره بود، در مسیر درستی هستید.
حرف آخر
CSRF از آن دسته حملاتی است که در ظاهر ساده، اما در باطن جدی است. تجربهام میگوید اگر امروز فقط یک کار بکنید، فهرست فرمهای سایت خود را مرور کنید و در همه آنها، توکن CSRF را بررسی کنید. اگر از فریمورکی مثل Django، وردپرس یا Laravel استفاده میکنید، این کار در حد چند خط تنظیم است. اگر از فرمهای سفارشی استفاده میکنید، همین یک قدم، شما را از یک حفره امنیتی جدی نجات میدهد. بعد از آن، SameSite روی کوکیها را فعال کنید و برای عملیات حساس، تأیید دوباره بگذارید. همین سه لایه، بیشترین بازدهی را در کمترین زمان دارند. اگر تجربهای از پیادهسازی CSRF در پروژهای دارید که در منابع فارسی کمتر گفته شده (مثلاً چالشهای CSRF در APIهای رندر سمت سرور)، در دیدگاهها بنویسید؛ همان تجربهها، تصویر این بحث را کاملتر میکنند. 🔐