فرض کنید وبسایت evil.com بدون اجازه شما بخواهد به bank.com درخواست بزند و موجودی حسابتان را بخواند. مرورگر چطور جلوی این کار را بگیرد؟
پاسخ: Same-Origin Policy و CORS.
Origin = Protocol + Domain + Port
https://api.example.com:443/users
└─┬─┘ └──────┬──────┘ └┬┘
│ │ │
Protocol Domain Port
مثالهای Same-Origin:
// Origin: https://example.com ✅ https://example.com/api/users // Same ✅ https://example.com:443/data // Port 443 پیشفرض HTTPS ❌ http://example.com // Protocol متفاوت ❌ https://api.example.com // Subdomain متفاوت ❌ https://example.com:8080 // Port متفاوت ❌ https://example.org // Domain متفاوت
سناریوی حمله بدون Same-Origin Policy:
// کاربر لاگین است در bank.com // حالا وارد evil.com میشود // evil.com میتواند این کار را بکند: fetch('https://bank.com/api/account/balance', { credentials: 'include' // Cookie های bank.com ارسال میشود }) .then(res => res.json()) .then(data => { // 💀 موجودی حساب کاربر را میدزدد sendToAttacker(data.balance); });
Same-Origin Policy جلوی این حمله را میگیرد:
مرورگر درخواست را ارسال میکند (نمیتواند جلوگیری کند)
سرور پاسخ میدهد
مرورگر پاسخ را به JavaScript نمیدهد ❌
┌─────────────┐ ┌─────────────┐
│ Frontend │ │ Backend │
│ (Browser) │ │ Server │
└──────┬──────┘ └──────┬──────┘
│ │
│ GET /api/users │
│ Origin: https://frontend.com │
├─────────────────────────────────>│
│ │
│ │ ✅ بررسی Origin
│ │
│ 200 OK │
│ Access-Control-Allow-Origin: │
│<─────────────────────────────────┤
│ │
✅ مرورگر │
پاسخ را تحویل │
JavaScript میدهد │
اگر Header نباشد:
// Console Error: // Access to fetch at 'https://api.example.com/users' // from origin 'https://frontend.com' has been blocked // by CORS policy: No 'Access-Control-Allow-Origin' // header is present on the requested resource.
درخواستهایی که بدون Preflight اجرا میشوند:
شرایط:
Method: فقط GET, POST, HEAD
Headers: فقط Safe Headers مثل Content-Type, Accept
Content-Type: فقط application/x-www-form-urlencoded, multipart/form-data, text/plain
مثال:
// ✅ Simple Request fetch('https://api.example.com/users', { method: 'GET', headers: { 'Accept': 'application/json' } })
پاسخ سرور:
HTTP/1.1 200 OK Access-Control-Allow-Origin: https://frontend.com Content-Type: application/json {"users": [...]
درخواستهایی که قبل از ارسال اصلی، یک درخواست OPTIONS ارسال میشود.
چه زمانی Preflight اتفاق میافتد؟
Method: PUT, DELETE, PATCH
Headers سفارشی: Authorization, X-Custom-Header
Content-Type: application/json
مثال:
// ❌ Preflight Request fetch('https://api.example.com/users', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer token123' }, body: JSON.stringify({ name: 'Ali' }) });
جریان کامل:
┌─────────────┐ ┌─────────────┐
│ Browser │ │ Server │
└──────┬──────┘ └──────┬──────┘
│ │
│ 1️⃣ OPTIONS /api/users (Preflight)│
│ Origin: https://frontend.com │
│ Access-Control-Request-Method: │
│ POST │
│ Access-Control-Request-Headers: │
│ Authorization, Content-Type │
├─────────────────────────────────>│
│ │
│ │ ✅ بررسی
│ │
│ 200 OK │
│ Access-Control-Allow-Origin: │
│ Access-Control-Allow-Methods: │
│ POST, GET, OPTIONS │
│ Access-Control-Allow-Headers: │
│ Authorization, Content-Type │
│ Access-Control-Max-Age: 86400 │
│<─────────────────────────────────┤
│ │
│ 2️⃣ POST /api/users (Actual) │
│ Authorization: Bearer token123 │
│ Content-Type: application/json │
│ Body: {“name”: “Ali”} │
├─────────────────────────────────>│
│ │
│ 201 Created │
│ Access-Control-Allow-Origin: │
│<─────────────────────────────────┤
Access-Control-Allow-Origin ⭐مهمترین Header - مشخص میکند کدام Origin مجاز است.
# ✅ یک Origin خاص Access-Control-Allow-Origin: https://frontend.com # ⚠️ همه (خطرناک برای API های حساس) Access-Control-Allow-Origin: * # ❌ چند Origin (غیرمجاز - باید Dynamic باشد) Access-Control-Allow-Origin: https://app1.com, https://app2.com
پیادهسازی Dynamic:
// NestJS @Controller('users') export class UsersController { @Get() getUsers(@Req() req: Request, @Res() res: Response) { const allowedOrigins = [ 'https://frontend.com', 'https://app.frontend.com' ]; const origin = req.headers.origin; if (allowedOrigins.includes(origin)) { res.setHeader('Access-Control-Allow-Origin', origin); } return res.json({ users: [...] }); } }
Access-Control-Allow-MethodsMethod های مجاز را مشخص میکند.
Access-Control-Allow-Methods: GET, POST, PUT, DELETE, OPTIONS
Header های سفارشی مجاز.
Access-Control-Allow-Headers: Authorization, Content-Type, X-Request-ID
Access-Control-Allow-Credentials 🔐اجازه ارسال Cookie و Authentication Headers.
Access-Control-Allow-Credentials: true
⚠️ نکته امنیتی:
# ❌ ترکیب خطرناک Access-Control-Allow-Origin: * Access-Control-Allow-Credentials: true # این ترکیب مجاز نیست! مرورگر Error میدهد.
✅ روش صحیح:
Access-Control-Allow-Origin: https://frontend.com Access-Control-Allow-Credentials: true
// Frontend fetch('https://api.example.com/profile', { credentials: 'include' // Cookie ها ارسال میشود });
Access-Control-Max-Ageمدت زمان Cache کردن Preflight (ثانیه).
Access-Control-Max-Age: 86400 # 24 ساعت
Header هایی که JavaScript میتواند بخواند.
Access-Control-Expose-Headers: X-Total-Count, X-Page-Number
بدون این Header:
fetch('https://api.example.com/users') .then(res => { console.log(res.headers.get('X-Total-Count')); // ❌ null });
با این Header:
// ✅ مقدار را میخواند console.log(res.headers.get('X-Total-Count')); // "150"
Originمرورگر خودکار اضافه میکند.
Origin: https://frontend.com
Access-Control-Request-Method (Preflight)Access-Control-Request-Method: DELETE
Access-Control-Request-Headers (Preflight)Access-Control-Request-Headers: Authorization, X-Custom-Header
// ✅ Postman/cURL - بدون محدودیت curl https://bank.com/api/balance // پاسخ را میگیرد حتی بدون CORS Headers // ❌ Browser - با محدودیت fetch('https://bank.com/api/balance') // Error: CORS policy blocked
چرا؟
سناریوی خطرناک (فقط در Browser):
// کاربر در gmail.com لاگین است // evil.com این کد را اجرا میکند: fetch('https://gmail.com/api/emails', { credentials: 'include' // Cookie های Gmail ارسال میشود }) .then(res => res.json()) .then(emails => { // 💀 ایمیلهای کاربر را میدزدد sendToAttacker(emails); });
CORS جلوی این حمله را میگیرد چون:
مرورگر درخواست را میفرستد (با Cookie)
Gmail پاسخ میدهد
مرورگر چک میکند: آیا Access-Control-Allow-Origin شامل evil.com است؟
❌ خیر → پاسخ را به JavaScript نمیدهد
// Backend (Node.js) const axios = require('axios'); // ✅ بدون مشکل const response = await axios.get('https://api.example.com/users');
دلیل:
Backend Context کاربر ندارد
Cookie های کاربر ارسال نمیشود
حمله CSRF/XSS معنا ندارد
// main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; async function bootstrap() { const app = await NestFactory.create(AppModule); // ✅ روش 1: ساده (Development) app.enableCors(); // ✅ روش 2: پیشرفته (Production) app.enableCors({ origin: ['https://frontend.com', 'https://app.frontend.com'], methods: ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'], allowedHeaders: ['Content-Type', 'Authorization'], credentials: true, maxAge: 3600, }); // ✅ روش 3: Dynamic app.enableCors({ origin: (origin, callback) => { const allowedOrigins = [ 'https://frontend.com', /\.example\.com$/, // همه subdomain های example.com ]; if (!origin || allowedOrigins.some(allowed => typeof allowed === 'string' ? allowed === origin : allowed.test(origin) )) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true, }); await app.listen(3000); } bootstrap();
const express = require('express'); const cors = require('cors'); const app = express(); // ✅ روش 1: همه Origins app.use(cors()); // ✅ روش 2: محدود app.use(cors({ origin: 'https://frontend.com', credentials: true, optionsSuccessStatus: 200 })); // ✅ روش 3: چند Origin const allowedOrigins = [ 'https://frontend.com', 'https://app.frontend.com' ]; app.use(cors({ origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } }, credentials: true })); // ✅ روش 4: Per-Route app.get('/public', cors(), (req, res) => { res.json({ message: 'Public API' }); }); app.get('/private', cors({ origin: 'https://frontend.com', credentials: true }), (req, res) => { res.json({ message: 'Private API' }); });
# settings.py INSTALLED_APPS = [ 'corsheaders', ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', 'django.middleware.common.CommonMiddleware', ] # ✅ Development CORS_ALLOW_ALL_ORIGINS = True # ✅ Production CORS_ALLOWED_ORIGINS = [ "https://frontend.com", "https://app.frontend.com", ] CORS_ALLOW_CREDENTIALS = True CORS_ALLOW_METHODS = [ 'DELETE', 'GET', 'OPTIONS', 'PATCH', 'POST', 'PUT', ] CORS_ALLOW_HEADERS = [ 'accept', 'authorization', 'content-type', 'x-csrf-token', ]
// config/cors.php return [ 'paths' => ['api/*'], 'allowed_methods' => ['*'], 'allowed_origins' => [ 'https://frontend.com', 'https://app.frontend.com', ], 'allowed_origins_patterns' => [ '/\.example\.com$/', ], 'allowed_headers' => ['*'], 'exposed_headers' => ['X-Total-Count'], 'max_age' => 3600, 'supports_credentials' => true, ];
Access to fetch at ‘https://api.example.com/users’
from origin ‘https://frontend.com’ has been blocked
by CORS policy: No ‘Access-Control-Allow-Origin’
header is present on the requested resource.
راهحل:
// Backend app.use(cors({ origin: 'https://frontend.com' }));
کد اشتباه:
// ❌ Backend app.use(cors({ origin: '*', credentials: true })); // Frontend fetch('https://api.example.com/profile', { credentials: 'include' });
Error:
The value of the ‘Access-Control-Allow-Origin’ header
must not be the wildcard ‘*’ when the request’s
credentials mode is ‘include’.
راهحل:
javascript// ✅ Backend app.use(cors({ origin: 'https://frontend.com', // Origin مشخص credentials: true }));
مشکل: Middleware های Authentication قبل از CORS اجرا میشوند.
// ❌ ترتیب اشتباه app.use(authMiddleware); // OPTIONS را Block میکند app.use(cors()); // ✅ ترتیب صحیح app.use(cors()); app.use(authMiddleware);
یا:
app.use((req, res, next) => { if (req.method === 'OPTIONS') { return res.sendStatus(200); // Preflight را بدون Auth پاس کن } next(); }); app.use(authMiddleware);
// Frontend: http://localhost:3000 // Backend: http://localhost:4000 // ❌ این کار نمیکند fetch('http://localhost:4000/api/users'); // Error: CORS // ✅ Backend باید اجازه بدهد app.use(cors({ origin: 'http://localhost:3000' }));
// Frontend fetch('https://api.example.com/users', { headers: { 'X-Custom-Header': 'value' } });
Error:
Request header field X-Custom-Header is not allowed
by Access-Control-Allow-Headers in preflight response.
راهحل:
// Backend app.use(cors({ allowedHeaders: ['Content-Type', 'Authorization', 'X-Custom-Header'] }));
origin: '*' با credentials: true استفاده نکنید// 💀 خطرناک app.use(cors({ origin: '*', credentials: true }));
چرا؟ هر سایتی میتواند با Cookie های کاربر به API شما درخواست بزند.
// ✅ فقط Origin های مجاز const allowedOrigins = [ 'https://frontend.com', 'https://app.frontend.com', process.env.NODE_ENV === 'development' && 'http://localhost:3000' ].filter(Boolean); app.use(cors({ origin: (origin, callback) => { if (!origin || allowedOrigins.includes(origin)) { callback(null, true); } else { callback(new Error('Not allowed by CORS')); } } }));
app.use(cors({ maxAge: 86400 // 24 ساعت }));
فایده: کاهش ۹۰٪ درخواستهای OPTIONS.
// ✅ فقط Method های مورد نیاز app.use(cors({ methods: ['GET', 'POST', 'PUT', 'DELETE'] })); // ❌ همه Method ها app.use(cors({ methods: '*' }));
app.use(cors({ origin: (origin, callback) => { if (process.env.NODE_ENV === 'production' && origin && !origin.startsWith('https://')) { callback(new Error('HTTPS required')); } else { callback(null, true); } } }));
javascript// Frontend (localhost:3000) fetch('https://api.example.com/users'); // Error: CORS
راهحل 1: فعال کردن CORS در Backend
javascript// Backend app.use(cors({ origin: 'http://localhost:3000' }));
راهحل 2: استفاده از Proxy (بدون تغییر Backend)
javascript// vite.config.js (Vite) export default { server: { proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '') } } } } // Frontend fetch('/api/users'); // ✅ بدون CORS Error // Vite آن را به https://api.example.com/users تبدیل میکند
javascript// webpack.config.js (React/Webpack) module.exports = { devServer: { proxy: { '/api': { target: 'https://api.example.com', changeOrigin: true, pathRewrite: { '^/api': '' } } } } }
مقایسه:
CORS:
✅ Production-ready
✅ کنترل دقیق
❌ نیاز به تغییر Backend
Proxy:
✅ فقط Development
✅ بدون تغییر Backend
❌ در Production کار نمیکند
┌──────────┐
│ Frontend │
└────┬─────┘
│
│
┌────▼─────────┐
│ API Gateway │ ← فقط اینجا CORS فعال است
└────┬─────────┘
│
├──────────┬──────────┬──────────┐
│ │ │ │
┌────▼────┐ ┌──▼───┐ ┌────▼────┐ ┌───▼────┐
│ Auth │ │ User │ │ Order │ │ Payment│
│ Service │ │Service│ │ Service │ │Service │
└─────────┘ └──────┘ └─────────┘ └────────┘
↑ ↑ ↑ ↑
└──────────┴──────────┴──────────┘
Internal (بدون CORS)
پیادهسازی:
content_copy javascript// API Gateway (NestJS) async function bootstrap() { const app = await NestFactory.create(AppModule); app.enableCors({ origin: 'https://frontend.com', credentials: true }); await app.listen(3000); } // Microservices (بدون CORS) // auth-service, user-service, order-service // هیچ CORS config ندارند چون فقط از Gateway صدا زده میشوند
content_copy bash# Preflight Request curl -X OPTIONS https://api.example.com/users \ -H "Origin: https://frontend.com" \ -H "Access-Control-Request-Method: POST" \ -H "Access-Control-Request-Headers: Authorization" \ -v # بررسی Response Headers: # Access-Control-Allow-Origin: https://frontend.com # Access-Control-Allow-Methods: POST, GET, OPTIONS # Access-Control-Allow-Headers: Authorization
content_copy bash# Actual Request curl -X POST https://api.example.com/users \ -H "Origin: https://frontend.com" \ -H "Authorization: Bearer token" \ -H "Content-Type: application/json" \ -d '{"name":"Ali"}' \ -v
content_copy typescript// cors.spec.ts import { Test } from '@nestjs/testing'; import { INestApplication } from '@nestjs/common'; import * as request from 'supertest'; import { AppModule } from './app.module'; describe('CORS', () => { let app: INestApplication; beforeAll(async () => { const moduleRef = await Test.createTestingModule({ imports: [AppModule], }).compile(); app = moduleRef.createNestApplication(); app.enableCors({ origin: 'https://frontend.com', credentials: true, }); await app.init(); }); it('should allow requests from allowed origin', () => { return request(app.getHttpServer()) .get('/users') .set('Origin', 'https://frontend.com') .expect(200) .expect('Access-Control-Allow-Origin', 'https://frontend.com'); }); it('should block requests from disallowed origin', () => { return request(app.getHttpServer()) .get('/users') .set('Origin', 'https://evil.com') .expect((res) => { expect(res.headers['access-control-allow-origin']).toBeUndefined(); }); }); it('should handle preflight correctly', () => { return request(app.getHttpServer()) .options('/users') .set('Origin', 'https://frontend.com') .set('Access-Control-Request-Method', 'POST') .set('Access-Control-Request-Headers', 'Authorization') .expect(200) .expect('Access-Control-Allow-Methods', /POST/) .expect('Access-Control-Allow-Headers', /Authorization/); }); afterAll(async () => { await app.close(); }); });
Q1: CORS چیست و چرا وجود دارد؟
A: مکانیزمی برای کنترل دسترسی Cross-Origin در مرورگر.
جلوی حملات XSS/CSRF را میگی