ویرگول
ورودثبت نام
Navid Barsalari
Navid Barsalariمهندس ارشد نرم‌افزار | تکنیکال لید | +۱۰ سال سابقه علاقه‌مند به System Design، توسعه بک‌اند (Go / Node.js) و معماری دیتابیس. تمرکز فعلی من روی ساخت و توسعه سرویس‌های مقیاس‌پذیر B2B است.
Navid Barsalari
Navid Barsalari
خواندن ۹ دقیقه·۴ روز پیش

CORS: راهنمای جامع از مفاهیم پایه تا Security Best Practices

مقدمه: مشکلی که CORS حل می‌کند

فرض کنید وبسایت evil.com بدون اجازه شما بخواهد به bank.com درخواست بزند و موجودی حسابتان را بخواند. مرورگر چطور جلوی این کار را بگیرد؟

پاسخ: Same-Origin Policy و CORS.

۱. Same-Origin Policy: دیوار امنیتی مرورگرها

۱.۱ تعریف Origin

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 نمی‌دهد ❌

۲.۱ نحوه کار CORS

┌─────────────┐ ┌─────────────┐

│ Frontend │ │ Backend │

│ (Browser) │ │ Server │

└──────┬──────┘ └──────┬──────┘

│ │

│ GET /api/users │

│ Origin: https://frontend.com │

├─────────────────────────────────>│

│ │

│ │ ✅ بررسی Origin

│ │

│ 200 OK │

│ Access-Control-Allow-Origin: │

│ https://frontend.com │

│<─────────────────────────────────┤

│ │

✅ مرورگر │

پاسخ را تحویل │

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.

۳. انواع درخواست‌های CORS

۳.۱ Simple Requests

درخواست‌هایی که بدون Preflight اجرا می‌شوند:

شرایط:

  1. Method: فقط GET, POST, HEAD

  2. Headers: فقط Safe Headers مثل Content-Type, Accept

  3. 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": [...]

۳.۲ Preflight Requests

درخواست‌هایی که قبل از ارسال اصلی، یک درخواست 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: │

│ https://frontend.com │

│ 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: │

│ https://frontend.com │

│<─────────────────────────────────┤

۴. CORS Headers: راهنمای کامل

۴.۱ Response Headers (سرور به مرورگر)

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-Methods

Method های مجاز را مشخص می‌کند.

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"

۴.۲ Request Headers (مرورگر به سرور)

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

۵. چرا CORS فقط در مرورگر است؟

۵.۱ تفاوت Browser vs Postman/cURL

// ✅ 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 جلوی این حمله را می‌گیرد چون:

  1. مرورگر درخواست را می‌فرستد (با Cookie)

  2. Gmail پاسخ می‌دهد

  3. مرورگر چک می‌کند: آیا Access-Control-Allow-Origin شامل evil.com است؟

  4. ❌ خیر → پاسخ را به JavaScript نمی‌دهد

// Backend (Node.js) const axios = require('axios'); // ✅ بدون مشکل const response = await axios.get('https://api.example.com/users');

دلیل:

  • Backend Context کاربر ندارد

  • Cookie های کاربر ارسال نمی‌شود

  • حمله CSRF/XSS معنا ندارد

۶. پیاده‌سازی CORS در Backend

۶.۱ NestJS

// 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();

۶.۲ Express.js

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' }); });

۶.۳ Django (Python)

# 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', ]

۶.۴ Laravel (PHP)

// 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, ];

۷. مشکلات رایج و راه‌حل

۷.۱ Error: No ‘Access-Control-Allow-Origin’ header

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' }));

**۷.۲ Error: Credentials flag is true, but Allow-Origin is *

کد اشتباه:

// ❌ 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 }));

۷.۳ Preflight OPTIONS درخواست 403 می‌دهد

مشکل: 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);

۷.۴ localhost با Port های مختلف

// Frontend: http://localhost:3000 // Backend: http://localhost:4000 // ❌ این کار نمی‌کند fetch('http://localhost:4000/api/users'); // Error: CORS // ✅ Backend باید اجازه بدهد app.use(cors({ origin: 'http://localhost:3000' }));

۷.۵ Custom Headers در Preflight

// 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'] }));

۸. Security Best Practices

۸.۱ هرگز origin: '*' با credentials: true استفاده نکنید

// 💀 خطرناک app.use(cors({ origin: '*', credentials: true }));

چرا؟ هر سایتی می‌تواند با Cookie های کاربر به API شما درخواست بزند.

۸.۲ Whitelist Origins را محدود کنید

// ✅ فقط 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')); } } }));

۸.۳ Preflight Caching

app.use(cors({ maxAge: 86400 // 24 ساعت }));

فایده: کاهش ۹۰٪ درخواست‌های OPTIONS.

۸.۴ محدود کردن Methods

// ✅ فقط Method های مورد نیاز app.use(cors({ methods: ['GET', 'POST', 'PUT', 'DELETE'] })); // ❌ همه Method ها app.use(cors({ methods: '*' }));

۸.۵ HTTPS اجباری در Production

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); } } }));

۹. CORS vs Proxy

۹.۱ مشکل CORS در Development

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 کار نمی‌کند


۱۰. CORS در معماری Microservices

۱۰.۱ API Gateway Pattern

┌──────────┐

│ Frontend │

└────┬─────┘

│

│ https://api.example.com

│

┌────▼─────────┐

│ 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 صدا زده می‌شوند

۱۱. Testing CORS

۱۱.۱ Manual Testing با cURL

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

۱۱.۲ Automated Testing (Jest)

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(); }); });

۱۲. سوالات مصاحبه

سطح Junior:

Q1: CORS چیست و چرا وجود دارد؟

A: مکانیزمی برای کنترل دسترسی Cross-Origin در مرورگر.

جلوی حملات XSS/CSRF را می‌گی

corssecuritybackendweb development
۲
۰
Navid Barsalari
Navid Barsalari
مهندس ارشد نرم‌افزار | تکنیکال لید | +۱۰ سال سابقه علاقه‌مند به System Design، توسعه بک‌اند (Go / Node.js) و معماری دیتابیس. تمرکز فعلی من روی ساخت و توسعه سرویس‌های مقیاس‌پذیر B2B است.
شاید از این پست‌ها خوشتان بیاید