`Cookie` ב-TypeScript

`Cookie` ב-TypeScript

מאמר זה מסביר על Cookie ב-TypeScript.

נעבור על דפוסים מעשיים לטיפול בעוגיות בצורה בטוחה ואמינה הן בדפדפן והן בשרת.

YouTube Video

Cookie ב-TypeScript

מושגי יסוד בעוגיות

עוגייה היא מנגנון לאחסון מחרוזות קצרות (זוגות name=value) בצד הלקוח, הנוצר באמצעות כותרת ה-HTTP ‏Set-Cookie או באמצעות document.cookie. ניתן לשלוט בהתנהגותן באמצעות מאפייני אבטחה (HttpOnly, Secure, SameSite וכו').

פעולות בסיסיות בדפדפן: document.cookie

להלן דוגמה מינימלית לכתיבת עוגייה בדפדפן. צרו עוגייה על ידי הוספת מחרוזת אל document.cookie.

 1// Set a simple cookie that expires in 7 days.
 2// Note: Comments are in English per the user's preference.
 3const setSimpleCookie = (name: string, value: string) => {
 4  const days = 7;
 5  const expires = new Date(Date.now() + days * 86400_000).toUTCString();
 6  // name=value; Expires=...; Path=/
 7  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; Expires=${expires}; Path=/`;
 8};
 9
10setSimpleCookie('theme', 'dark');
  • קוד זה יוצר עוגייה שפגה לאחר 7 ימים. קל להשתמש בו בצד הדפדפן, אך מאחר שלא ניתן להגדיר HttpOnly, יש להימנע מאחסון מידע רגיש.

קריאת עוגיות (דפדפן)

הפונקציה הבאה היא פונקציית עזר שמאחזרת את ערך העוגייה בעלת שם מסוים מתוך document.cookie. document.cookie מוחזר כמחרוזת שמופרדת באמצעות נקודה-פסיק ורווח ('; '). לכן אנו מפצלים אותה ומחפשים את העוגייה הרצויה.

 1// Parse document.cookie and return value for given name or null if not found.
 2const getCookie = (name: string): string | null => {
 3  const cookies = document.cookie ? document.cookie.split('; ') : [];
 4  for (const cookie of cookies) {
 5    const [k, ...rest] = cookie.split('=');
 6    const v = rest.join('=');
 7    if (decodeURIComponent(k) === name) {
 8      return decodeURIComponent(v);
 9    }
10  }
11  return null;
12};
13
14// Example usage:
15const theme = getCookie('theme'); // => "dark" if set
16console.log('theme cookie:', theme);
  • הפונקציה הזאת מפענחת גם את שם העוגייה וגם את ערכה כדי להשוות ולאחזר אותם בבטחה. היא מטפלת בשמות עוגיות כפולים ובתווים מיוחדים מקודדים, מה שהופך אותה למימוש פשוט אך עמיד.

הבנה ויישום של מאפייני אבטחה

לעוגיות יש כמה מאפיינים חשובים, שכל אחד מהם שולט באבטחה ובתחום הפעולה.

  • המאפיין HttpOnly מונע מהעוגייה להיות נגישה מ-JavaScript, ובכך מסייע להפחית מתקפות XSS (Cross‑Site Scripting). שימו לב שלא ניתן להגדיר HttpOnly מצדו של הדפדפן.
  • המאפיין Secure מגביל שליחת עוגיות ל-HTTPS בלבד, ומפחית את הסיכון להאזנה ולשינוי זדוני.
  • המאפיין SameSite קובע האם עוגיות יישלחו בבקשות חוצות-אתרים, ומסייע למנוע מתקפות CSRF (Cross‑Site Request Forgery).
  • המאפיין Path מגדיר את תחום נתיבי הבקשות שעבורם נשלחת העוגייה, והמאפיין Domain מגדיר את הדומיין שבו העוגייה תקפה.
  • באמצעות הגדרת Expires או Max-Age ניתן לשלוט בתפוגת העוגייה.

דוגמה להוספת SameSite / Secure בדפדפן (מה שאפשרי)

לא ניתן להגדיר HttpOnly בצד הלקוח. להלן דוגמה להוספת SameSite ו-Secure בצד הלקוח. עם זאת, Secure נכנס לתוקף רק בדפי HTTPS.

 1// Set cookie with SameSite and Secure attributes (Secure only effective over HTTPS).
 2const setCookieWithAttributes = (name: string, value: string) => {
 3  const maxAge = 60 * 60 * 24 * 7; // 7 days in seconds
 4  // Note: HttpOnly cannot be set from JS; set it on server-side when you want to restrict JS access.
 5  document.cookie = `${encodeURIComponent(name)}=${encodeURIComponent(value)}; `
 6    + `Max-Age=${maxAge}; `
 7    + `Path=/; `
 8    + `SameSite=Lax; `
 9    + `Secure`;
10};
11
12setCookieWithAttributes('session_hint', 'true');
  • SameSite=Lax הוא בחירה בטוחה ונפוצה. למגבלות מחמירות יותר השתמשו ב-SameSite=Strict, אך ניווטים לגיטימיים מאתרים חיצוניים עלולים להפסיק לעבוד.

הגדרת עוגיות בשרת (Set-Cookie) (Node / TypeScript)

בשרת ניתן להוסיף HttpOnly באמצעות כותרת Set-Cookie, ולכן ניהול סשנים רצוי שייעשה בצד השרת. להלן דוגמה באמצעות המודול http של Node.

 1// A minimal Node HTTP server in TypeScript that sets a secure HttpOnly cookie.
 2// This example uses built-in 'crypto' for a random session id.
 3import http from 'http';
 4import crypto from 'crypto';
 5
 6const server = http.createServer((req, res) => {
 7  if (req.url === '/login') {
 8    const sessionId = crypto.randomBytes(16).toString('hex');
 9    // Set cookie with HttpOnly, Secure, SameSite and Path
10    // Expires is optional — Max-Age preferred for relative lifetimes.
11    res.setHeader('Set-Cookie', `sid=${sessionId}; `
12      + `HttpOnly; `
13      + `Secure; `
14      + `SameSite=Strict; `
15      + `Path=/; `
16      + `Max-Age=3600`
17    );
18    res.writeHead(302, { Location: '/' });
19    res.end();
20    return;
21  }
22
23  res.writeHead(200, { 'Content-Type': 'text/plain' });
24  res.end('Hello\n');
25});
26
27server.listen(3000, () => {
28  console.log('Server running on http://localhost:3000');
29});
  • כאשר מגדירים את HttpOnly בצד השרת, העוגייה אינה נגישה מ-JavaScript, מה שמקשה על מתקפות XSS (Cross‑Site Scripting) לגנוב אותה. בנוסף, על ידי הוספת Secure כך שעוגיות יישלחו תמיד דרך HTTPS, ניתן למנוע האזנה ושינוי זדוני ולשפר את אבטחת התקשורת.

מימוש סיריאליזציה/פענוח של עוגיות (עזר בצד השרת)

להלן כלי עזר פשוט בצד השרת שבונה מחרוזת כותרת Set-Cookie.

 1// Cookie serialization helper for server-side use.
 2// Returns a properly formatted Set-Cookie header value.
 3type CookieOptions = {
 4  path?: string;
 5  domain?: string;
 6  maxAge?: number;
 7  expires?: Date;
 8  httpOnly?: boolean;
 9  secure?: boolean;
10  sameSite?: 'Strict' | 'Lax' | 'None';
11};
12
13const serializeCookie = (name: string, value: string, opts: CookieOptions = {}): string => {
14  const parts: string[] = [`${encodeURIComponent(name)}=${encodeURIComponent(value)}`];
15
16  if (opts.maxAge != null) parts.push(`Max-Age=${Math.floor(opts.maxAge)}`);
17  if (opts.expires) parts.push(`Expires=${opts.expires.toUTCString()}`);
18  if (opts.domain) parts.push(`Domain=${opts.domain}`);
19  parts.push(`Path=${opts.path ?? '/'}`);
20  if (opts.httpOnly) parts.push('HttpOnly');
21  if (opts.secure) parts.push('Secure');
22  if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
23
24  return parts.join('; ');
25};
26
27// Example usage:
28const headerValue = serializeCookie('uid', 'abc123', {
29  httpOnly: true,
30  secure: true,
31  sameSite: 'Lax',
32  maxAge: 3600
33});
34console.log(headerValue);
35// => "uid=abc123; Max-Age=3600; Path=/; HttpOnly; Secure; SameSite=Lax"
  • כלי עזר זה מהווה בסיס ליצירת Set-Cookie בשרת מותאם אישית. ספריות יכולות לטפל בעוד מקרים קיצוניים רבים.

מניעת שינוי ערכים באמצעות חתימות (HMAC)

מסוכן לאחסן ערכים חשובים ישירות בעוגיות. נציג שיטה שחותמת ערכים בשרת כדי לזהות שינוי. כאן נשתמש ב-HMAC-SHA256. בסביבת ייצור, יש לנהל גם שלילת טוקנים בצד השרת.

 1// A simple HMAC signing and verification helper for cookies.
 2// Signing prevents client from tampering cookie values.
 3import crypto from 'crypto';
 4
 5const SECRET = 'replace_with_env_secret'; // store in env var in production
 6
 7const signValue = (value: string) => {
 8  const hmac = crypto.createHmac('sha256', SECRET);
 9  hmac.update(value);
10  return `${value}.${hmac.digest('hex')}`;
11};
12
13const verifySignedValue = (signed: string): string | null => {
14  const idx = signed.lastIndexOf('.');
15  if (idx === -1) return null;
16  const value = signed.slice(0, idx);
17  const sig = signed.slice(idx + 1);
18
19  const hmac = crypto.createHmac('sha256', SECRET);
20  hmac.update(value);
21  const expected = hmac.digest('hex');
22  // use timing-safe comparison in production
23  if (crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
24    return value;
25  }
26  return null;
27};
28
29// Example usage:
30const signed = signValue('userId=42');
31console.log('signed:', signed);
32console.log('verified:', verifySignedValue(signed)); // => "userId=42"
  • באמצעות שיטה זו ניתן לבדוק אם ערך ה-Cookie שונה. אין לכלול את મפתח החתימה בקוד המקור; יש לנהל אותו באמצעים מאובטחים כגון משתני סביבה.

עוגיות וצמצום CSRF (דפוסים כלליים)

סשנים המשתמשים בעוגיות זקוקים להגנת CSRF. אמצעי מיתון טיפוסיים כוללים:.

  • הגדירו SameSite ל-Lax או Strict כדי למנוע שליחה בין-אתרית מיותרת. השתמשו ב-SameSite=None; Secure רק כאשר תקשורת בין-מקורות נחוצה.
  • השתמשו באסימוני CSRF, המאומתים על ידי השרת, עבור טפסים ובקשות API. אל תאחסנו את האסימון בעוגיית HttpOnly; במקום זאת, ספקו אותו דרך גוף התגובה או תגיות meta, ותנו ל-JavaScript לקרוא אותו רק בעת הצורך.
  • אם ניתן לגשת לאסימון מ-JavaScript, ודאו שגם מיושמות הגנות מפני XSS (CSP, אסקייפינג של פלט וכו׳).
  • דרשו אימות מחדש עבור פעולות רגישות ובכניסה, ומנעו מתקפות קיבוע מושב.

להלן דוגמה ללקוח מאובטח ששולח את אסימון ה-CSRF בכותרת. זה מניח שהלקוח כבר קיבל את האסימון מהשרת, למשל בגוף התגובה.

 1// Example of sending CSRF token safely in a header using fetch.
 2// Assumes CSRF token was provided securely (e.g., via response body or meta tag).
 3async function sendProtectedRequest(url: string, csrfToken: string) {
 4  const res = await fetch(url, {
 5    method: 'POST',
 6    credentials: 'same-origin', // include cookies for same-site requests
 7    headers: {
 8      'Content-Type': 'application/json',
 9      'X-CSRF-Token': csrfToken
10    },
11    body: JSON.stringify({ action: 'doSomething' })
12  });
13  return res;
14}
  • אם מציינים credentials: 'same-origin', נשלחות רק עוגיות עבור אותו מקור. בשרת, אמתו את ערך כותרת X-CSRF-Token ואשרו שהאסימון תואם. אם בקשות בין-אתריות נחוצות, הגדירו את CORS בקפידה.

בין-אתרי (CORS) והקשר ל-credentials

כדי לשלוח ולקבל עוגיות בתקשורת בין-מקורות, הלקוח חייב לציין credentials: 'include' והשרת חייב להגדיר Access-Control-Allow-Credentials: true. עם זאת, הגבילו תצורה זו למקורות מהימנים ואל תשתמשו ב-Access-Control-Allow-Origin: *.

 1async function fetchCrossOriginData() {
 2  // Example: cross-origin fetch sending cookies.
 3  // Server must set Access-Control-Allow-Credentials: true
 4  // and a specific trusted origin.
 5  const res = await fetch('https://api.example.com/data', {
 6    credentials: 'include', // send cookies only to trusted domains
 7    method: 'GET'
 8  });
 9  return res;
10}
  • השילוב בין CORS לעוגיות רגיש מאוד מבחינת אבטחה. נהלו בקפדנות את המקורות המותרים באמצעות רשימת מורשים (whitelist) והימנעו מתקשורת בין-אתרית מיותרת. בנוסף, בעת שימוש ב-SameSite=None; Secure, אכפו HTTPS כדי למנוע מתקפות אדם-בתווך.

דוגמה: הגדרות עוגיית סשן מאובטחות עם Express (TypeScript)

בפועל, השתמשו ב-Express, בספריית cookie, ב-express-session וכדומה. להלן דוגמה פשוטה באמצעות express ו-cookie-parser. בפועל, הגדירו secure ל-true ונהלו את ה-secret באמצעות משתני סביבה.

 1// Express example using cookie-parser and setting a secure httpOnly cookie.
 2// npm install express cookie-parser @types/express @types/cookie-parser
 3import express from 'express';
 4import cookieParser from 'cookie-parser';
 5
 6const app = express();
 7app.use(cookieParser(process.env.COOKIE_SECRET));
 8
 9app.post('/login', (req, res) => {
10  // authenticate user (omitted)
11  const sessionId = 'generated-session-id';
12  res.cookie('sid', sessionId, {
13    httpOnly: true,
14    secure: true,        // require HTTPS in production
15    sameSite: 'lax',
16    maxAge: 1000 * 60 * 60 // 1 hour
17  });
18  res.json({ ok: true });
19});
20
21app.listen(3000);
  • באמצעות התכונה secret של cookieParser, ניתן לעבוד בקלות עם עוגיות חתומות. אולם, ביישומים אמיתיים, מבחינות של אבטחה וסקיילביליות, אין לאחסן נתונים ישירות בעוגיות; במקום זאת, השתמשו במאגר סשן ייעודי.

קידומות עוגיות __Host- ו-__Secure-

דפדפנים אוכפים כללים מיוחדים עבור קידומות מסוימות.

  • הקידומת __Secure- אם שם העוגייה מתחיל ב-__Secure-, המאפיין Secure נדרש.
  • הקידומת __Host- אם הוא מתחיל ב-__Host-, יש לדרוש Secure, הנתיב חייב להיות / (שורש), ואסור להגדיר Domain.

השימוש בקידומות הללו מפחית טעויות תצורה ומשפר את האבטחה.

נהלים מומלצים לעוגיות

כדי לטפל בקובצי Cookie באופן מאובטח, יש לשקול את הנקודות הבאות.

  • אל תאחסנו מידע רגיש ישירות בעוגיות. העדיפו מזהה סשן בצד השרת עבור אסימוני גישה.
  • הגדירו עוגיות סשן עם HttpOnly, ‏Secure ו-SameSite=Lax (או Strict).
  • נצלו קידומות כמו __Host- ו-__Secure-.
  • שקלו חתימות (HMAC) והצפנה כדי למנוע שינוי והאזנה.
  • הפעילו Secure ודרשו HTTPS.
  • שאפו לעיקרון המינימום הנדרש ולזמני תפוגה קצרים.
  • השתמשו באסימוני CSRF.
  • היזהרו מהבדלים בהתנהגות SameSite בין דפדפנים, במיוחד ישנים.

טעויות נפוצות לגבי עוגיות

בנוגע לעוגיות, שימו לב לתפיסות השגויות הנפוצות הבאות.

  • 'הוספת HttpOnly מבטלת את השפעת ה-XSS.' אמנם HttpOnly מונע גישה של JavaScript לעוגיות, אך ניתן עדיין לנצל XSS כדי לבצע בקשות שרירותיות. יש ליישם גם אימות טוקן CSRF, CSP (מדיניות אבטחת תוכן), וטיהור קלט.
  • 'Secure מיותר לפיתוח מקומי.' סימולציה של HTTPS ואימות ההתנהגות גם בסביבות מקומיות משפרים את דיוק הבדיקות. לכל הפחות, יש להשתמש ב-HTTPS בסביבות Staging ו-Production.
  • 'תקופות תפוגה ארוכות הן נוחות.' אם תקבעו לעוגיות משך חיים ארוך, מתארכת התקופה שבה ניתן יהיה לנצלן אם ייגנבו. ניתן להגדיר תפוגות קצרות יותר ולשלב אימות חוזר תקופתי ורוטציית אסימונים.

סיכום

למרות שעוגיות קלות לשימוש, טיפול לקוי בהן עלול להכניס פגיעויות אבטחה. כדי לנהל אותן כראוי עם TypeScript, חשוב להבין מאפיינים כמו HttpOnly, ‏Secure ו-SameSite, ולאכוף הגדרות מאובטחות בצד השרת. באמצעות הימנעות מאחסון ישיר של נתונים רגישים ושילוב חתימות עם זמני תפוגה קצרים, ניתן להשיג ניהול סשן בטוח ואמין.

תוכלו לעקוב אחר המאמר שלמעלה באמצעות Visual Studio Code בערוץ היוטיוב שלנו. נא לבדוק גם את ערוץ היוטיוב.

YouTube Video