مدل امنیتی HTML5: از CORS تا CSP و Sandbox کردن iframe

خیلیا فکر می‌کنن HTML فقط یه سری تگه که ظاهر صفحه رو می‌سازه. ولی وقتی پای امنیت وسط میاد، HTML5 یهو تبدیل میشه به یه لایه‌ی جدی که مستقیم با مرورگر و شبکه درگیره. تو این مقاله می‌خوایم بریم زیر پوست چهار تا از مهم‌ترین مکانیزم‌های امنیتی وب: Same-Origin Policy، CORS، CSP و Sandbox کردن iframe. اینا چیزایی نیستن که با یه alt یا یه data-attribute حل بشن؛ اینا تصمیمای معماری‌ان.

۱. همه چی از Same-Origin Policy شروع میشه

قبل از هر چیزی باید بدونیم "Origin" یعنی چی. یه origin ترکیبیه از سه چیز: scheme (پروتکل)، host (دامنه) و port. اگه یکی از این سه‌تا فرق کنه، از نظر مرورگر با یه origin کاملاً متفاوت طرفیم.

https://example.com:443/page1
https://example.com:443/page2   → همون origin (فرقی نداره path)

http://example.com              → origin متفاوت (scheme فرق داره)
https://api.example.com         → origin متفاوت (subdomain فرق داره)
https://example.com:8080        → origin متفاوت (port فرق داره)

Same-Origin Policy (SOP) یه قانون پایه‌ست که میگه: جاوااسکریپت تو یه صفحه، فقط می‌تونه به منابع همون origin دسترسی مستقیم داشته باشه (مثلاً خوندن response یه fetch، یا دسترسی به DOM یه iframe دیگه). این قانون از اول وب بوده و پایه‌ی امنیت مرورگره. اما دنیای واقعی نیاز داره که سایت‌ها با هم دیگه حرف بزنن، برای همین CORS اومد.

۲. CORS: دروازه‌ی کنترل‌شده برای عبور از Origin

CORS (Cross-Origin Resource Sharing) یه مکانیزمه که به سرور اجازه میده صریحاً بگه "کدوم origin‌ها اجازه دارن به منابع من دسترسی داشته باشن". نکته‌ی مهم اینه: CORS یه مکانیزم سمت سروره، نه سمت کلاینت. مرورگر همیشه درخواست رو می‌فرسته؛ فقط اگه هدرهای پاسخ درست نباشه، جلوی خوندن نتیجه رو تو جاوااسکریپت می‌گیره.

درخواست‌های ساده در برابر Preflight

اگه درخواستت "ساده" باشه (مثلاً GET با هدرهای استاندارد)، مرورگر مستقیم می‌فرستدش. اما اگه از متدهایی مثل PUT، DELETE استفاده کنی یا هدر سفارشی بذاری یا Content-Type غیر استاندارد باشه، مرورگر اول یه درخواست OPTIONS به اسم Preflight می‌فرسته تا از سرور اجازه بگیره.

OPTIONS /api/user HTTP/1.1
Origin: https://app.example.com
Access-Control-Request-Method: PUT
Access-Control-Request-Headers: content-type, authorization

سرور باید با این هدرها جواب بده تا مرورگر اجازه‌ی ادامه بده:

HTTP/1.1 204 No Content
Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Methods: GET, PUT, POST, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization
Access-Control-Max-Age: 86400

قضیه‌ی credentials که خیلیا اشتباه می‌کنن

اگه بخوای کوکی یا اطلاعات هویتی با درخواست cross-origin بفرستی، باید تو جاوااسکریپت صریحاً بگی:

fetch('https://api.example.com/data', {
  credentials: 'include'
});

ولی این کافی نیست. سرور هم باید صریحاً اجازه بده، و اینجا یه نکته‌ی حیاتیه:

Access-Control-Allow-Origin: https://app.example.com
Access-Control-Allow-Credentials: true

وقتی Access-Control-Allow-Credentials برابر true باشه، مرورگر اجازه نمیده از wildcard (*) تو Access-Control-Allow-Origin استفاده کنی. سرور مجبوره origin رو دقیق و اکو-شده برگردونه. این یه تصمیم امنیتی عمدیه، چون اگه wildcard با credentials مجاز بود، هر سایتی می‌تونست با کوکی‌های کاربر به API‌های خصوصی درخواست بزنه.

۳. CSP: دیوار دفاعی در برابر XSS

Content Security Policy یه هدر (یا meta tag) هست که به مرورگر میگه از کجا اجازه داره محتوا (اسکریپت، استایل، عکس، فونت و...) بارگذاری کنه. هدفش اینه که حتی اگه یه مهاجم بتونه کد تزریق کنه (XSS)، مرورگر از اجراش سر باز بزنه.

Content-Security-Policy: 
  default-src 'self';
  script-src 'self' https://cdn.example.com;
  style-src 'self' 'unsafe-inline';
  img-src 'self' data: https:;
  connect-src 'self' https://api.example.com;
  frame-ancestors 'none';
  object-src 'none';
  base-uri 'self';

یا اگه بخوای از طریق HTML بذاریش (کمتر توصیه میشه چون بعضی دایرکتیوها مثل frame-ancestors تو meta کار نمی‌کنن):

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self'">

چرا inline script خطرناکه و nonce چطور حلش می‌کنه؟

CSP به‌صورت پیش‌فرض اسکریپت‌های inline رو مسدود می‌کنه، چون اگه مهاجم بتونه یه <script> داخل صفحه تزریق کنه (مثلاً از طریق یه فرم ناامن)، CSP جلوی اجراش رو می‌گیره. اما اگه واقعاً به inline script نیاز داری، از nonce استفاده می‌کنی؛ یه توکن تصادفی که هر بار سرور تولیدش می‌کنه:

Content-Security-Policy: script-src 'nonce-r4nd0mABC123'
<script nonce="r4nd0mABC123">
  console.log('این اسکریپت مجازه چون nonce داره');
</script>

نکته‌ی مهم: nonce باید هر بار رندر صفحه تازه تولید بشه (نه یه مقدار ثابت تو کد). وگرنه مهاجم می‌تونه همون nonce رو تو اسکریپت تزریقی خودش کپی کنه و کل مکانیزم بی‌فایده میشه.

راه‌حل جایگزین: hash-based CSP

Content-Security-Policy: script-src 'sha256-abc123hashvalue...'

اینجا مرورگر هش محتوای اسکریپت رو حساب می‌کنه و با مقدار مجاز مقایسه می‌کنه. اگه حتی یه کاراکتر فرق کنه، اجرا نمیشه. برای اسکریپت‌های استاتیک که تغییر نمی‌کنن، این روش امن‌تر و ساده‌تر از nonce‌ه.

۴. Sandbox کردن iframe: کنترل ریز به ریز

وقتی محتوای شخص ثالث رو تو iframe نمایش میدی (مثلاً یه ویجت تبلیغاتی یا کد کاربر تو یه پلتفرم)، به‌صورت پیش‌فرض اون iframe همون قابلیت‌های یه صفحه‌ی عادی رو داره: می‌تونه فرم بفرسته، پاپ‌آپ باز کنه، اسکریپت اجرا کنه. attribute sandbox بهت اجازه میده این قابلیت‌ها رو یکی‌یکی محدود کنی.

<iframe src="https://widget.thirdparty.com/embed" 
        sandbox="allow-scripts allow-forms">
</iframe>

اگه sandbox رو بدون هیچ مقداری بذاری، سخت‌گیرترین حالت اعمال میشه: نه اسکریپت، نه فرم، نه پاپ‌آپ، نه navigation از داخل، هیچی. هر تیکه اجازه (flag) رو باید صریحاً اضافه کنی:

Flagچی رو باز می‌کنه
allow-scriptsاجازه‌ی اجرای جاوااسکریپت
allow-formsاجازه‌ی ارسال فرم
allow-popupsاجازه‌ی باز کردن پنجره/تب جدید
allow-same-originiframe با origin واقعی خودش رفتار کنه، نه یه origin تهی
allow-top-navigationاجازه‌ی تغییر آدرس صفحه‌ی اصلی (parent)

ترکیب خطرناک: allow-scripts + allow-same-origin

اینجا یه نکته‌ی خیلی مهمه که خیلیا ازش غافلن. اگه هم‌زمان allow-scripts و allow-same-origin رو بدی، عملاً sandbox رو بی‌اثر کردی! چون اگه iframe بتونه هم اسکریپت اجرا کنه و هم origin واقعی خودش رو داشته باشه، می‌تونه از طریق جاوااسکریپت خود attribute sandbox رو از DOM حذف کنه و کاملاً آزاد بشه (تو navigationـهای بعدی).

<!-- خطرناک: عملاً sandbox رو خنثی می‌کنه -->
<iframe src="https://untrusted.com" 
        sandbox="allow-scripts allow-same-origin">
</iframe>

قانون کلی: اگه به محتوای iframe اعتماد نداری، این دوتا رو هیچ‌وقت با هم نده. یا فقط allow-scripts بده (و بدون same-origin بذار توی یه origin تهی و ایزوله اجرا بشه)، یا اگه واقعاً به same-origin نیاز داری، مطمئن شو منبع قابل‌اعتماده.

allow: کنترل روی Permission‌های مرورگر

جدا از sandbox، attribute allow کنترل می‌کنه که iframe به کدوم API‌های حساس مرورگر (مثل دوربین، میکروفون، لوکیشن) دسترسی داشته باشه؛ این همون Permissions Policy‌ه:

<iframe src="https://video-call.example.com"
        allow="camera 'self'; microphone 'self'; geolocation 'none'">
</iframe>

frame-ancestors: جلوگیری از Clickjacking

یه سوال دیگه اینه: چطور جلوی این رو بگیریم که سایت خودت داخل iframe یه سایت مخرب دیگه لود بشه (حمله‌ی Clickjacking)؟ اینجا دیگه از سمت خود صفحه‌ای که می‌خواد embed بشه کنترل می‌کنیم، با دایرکتیو frame-ancestors تو CSP (جایگزین مدرن هدر قدیمی X-Frame-Options):

Content-Security-Policy: frame-ancestors 'self' https://trusted-partner.com

این یعنی: فقط خود سایت و trusted-partner.com اجازه دارن این صفحه رو داخل iframe بذارن، هر سایت دیگه‌ای که سعی کنه، مرورگر جلوش رو می‌گیره.

۵. ارتباط امن بین iframe و صفحه‌ی اصلی: postMessage

وقتی iframe و صفحه‌ی parent origin متفاوت دارن، تنها راه ارتباط امن postMessageه. اما خیلیا اشتباه رایج می‌کنن و target origin رو * می‌ذارن که یعنی "به هر کی گوش میده بفرست"، این خودش یه حفره‌ی امنیتیه.

// طرف فرستنده - همیشه origin دقیق رو مشخص کن
targetWindow.postMessage({ type: 'auth', token: '...' }, 'https://trusted-app.com');

// طرف گیرنده - همیشه origin پیام ورودی رو چک کن
window.addEventListener('message', (event) => {
  if (event.origin !== 'https://trusted-app.com') {
    return; // نادیده بگیر، این پیام قابل اعتماد نیست
  }
  console.log('پیام معتبر:', event.data);
});

اگه origin رو تو گیرنده چک نکنی، هر iframe یا پنجره‌ی دیگه‌ای که رفرنسی به این window داشته باشه می‌تونه پیام جعلی بفرسته و منطق برنامه‌ت رو گول بزنه.

۶. یه لایه‌ی دیگه: Subresource Integrity (SRI)

وقتی از CDN شخص ثالث اسکریپت لود می‌کنی، از کجا مطمئنی محتواش تغییر نکرده (مثلاً CDN هک شده)؟ اینجا SRI کمک می‌کنه؛ هش فایل رو تو HTML می‌ذاری و مرورگر قبل از اجرا چکش می‌کنه:

<script src="https://cdn.example.com/library.js"
        integrity="sha384-oqVuAfXRKap7fdgcCY5uykM6+R9GqQ8K/uxy9rx7HNQlGYl1kPzQho1wx4JwY8wC"
        crossorigin="anonymous">
</script>

اگه هش فایل دانلود‌شده با مقدار داخل integrity یکی نباشه، مرورگر از اجراش خودداری می‌کنه. توجه کن که crossorigin="anonymous" هم لازمه، چون SRI برای محاسبه‌ی درست نیاز به دسترسی cross-origin به محتوای فایل داره.

جمع‌بندی

چیزی که این چهار-پنج مکانیزم رو به هم وصل می‌کنه، یه فلسفه‌ی مشترکه: مرورگر به‌صورت پیش‌فرض بی‌اعتماده و سرور/صفحه باید صریحاً اعتماد بده. CORS میگه کی اجازه داره بخونه. CSP میگه چی اجازه داره اجرا بشه. Sandbox میگه iframe چه قدرتی داره. SRI میگه محتوا دست‌نخورده مونده یا نه. یاد گرفتن این لایه‌ها همونیه که HTML رو از یه زبان markup ساده به یه سطح جدی از مهندسی امنیت وب تبدیل می‌کنه.