`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"
  • ด้วยวิธีนี้ คุณสามารถตรวจสอบได้ว่าค่าของคุกกี้ถูกดัดแปลงหรือไม่ อย่าใส่คีย์สำหรับการลงนามไว้ในซอร์สโค้ด; ควรจัดการผ่านวิธีที่ปลอดภัย เช่น ตัวแปรสภาพแวดล้อม

คุกกี้และการลดความเสี่ยง CSRF (รูปแบบทั่วไป)

เซสชันที่ใช้คุกกี้จำเป็นต้องมีการป้องกัน CSRF แนวทางลดความเสี่ยงที่พบบ่อย ได้แก่:

  • ตั้งค่า SameSite เป็น Lax หรือ Strict เพื่อป้องกันการส่งข้ามไซต์ที่ไม่จำเป็น ใช้ SameSite=None; Secure เฉพาะเมื่อจำเป็นต้องมีการสื่อสารข้ามต้นทางเท่านั้น
  • ใช้โทเค็น CSRF ซึ่งได้รับการตรวจสอบความถูกต้องโดยเซิร์ฟเวอร์ สำหรับแบบฟอร์มและคำขอ API ห้ามเก็บโทเค็นไว้ในคุกกี้แบบ HttpOnly; ให้ ส่งมอบผ่านเนื้อหาของการตอบกลับ (response body) หรือแท็ก meta และให้ JavaScript อ่านเมื่อจำเป็นเท่านั้น
  • หากสามารถเข้าถึงโทเค็นจาก JavaScript ได้ ควรแน่ใจว่ามีการใช้มาตรการป้องกัน XSS ด้วย (CSP, การ escape เอาต์พุต เป็นต้น)
  • กำหนดให้ยืนยันตัวตนอีกครั้งสำหรับการกระทำที่อ่อนไหวและในขั้นตอนล็อกอิน และป้องกันการโจมตีแบบตรึงเซสชัน (session fixation)

ด้านล่างคือตัวอย่างไคลเอนต์ที่ปลอดภัยซึ่งส่งโทเค็น CSRF ในเฮดเดอร์ ข้อนี้ถือว่าไคลเอนต์ได้รับโทเค็นจากเซิร์ฟเวอร์มาแล้ว เช่น ในเนื้อหาของการตอบกลับ (response body)

 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 เพื่อป้องกันการโจมตีแบบคนกลาง (man-in-the-middle)

ตัวอย่าง: การตั้งค่าคุกกี้เซสชันอย่างปลอดภัยด้วย 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

การใช้คำนำหน้าเหล่านี้ช่วยลดการตั้งค่าที่ผิดพลาดและเพิ่มความปลอดภัย

แนวปฏิบัติที่ดีที่สุดสำหรับคุกกี้

เพื่อจัดการคุกกี้อย่างปลอดภัย โปรดพิจารณาประเด็นต่อไปนี้

  • อย่าเก็บข้อมูลอ่อนไหวโดยตรงในคุกกี้ ควรใช้รหัสเซสชันฝั่งเซิร์ฟเวอร์สำหรับโทเคนการเข้าถึง
  • ตั้งค่าคุกกี้เซสชันด้วย HttpOnly, Secure และ SameSite=Lax (หรือ Strict)
  • ใช้ประโยชน์จากคำนำหน้าอย่าง __Host- และ __Secure-
  • พิจารณาการลงลายเซ็น (HMAC) และการเข้ารหัสเพื่อป้องกันการปลอมแปลงและการดักฟัง
  • เปิดใช้ Secure และบังคับใช้ HTTPS
  • มุ่งสู่สิทธิ์ต่ำสุดและอายุคุกกี้ที่สั้น
  • ใช้โทเคน CSRF
  • ระวังความแตกต่างของพฤติกรรม SameSite ระหว่างเบราว์เซอร์ โดยเฉพาะรุ่นเก่า

ความเข้าใจผิดที่พบบ่อยเกี่ยวกับคุกกี้

เกี่ยวกับคุกกี้ โปรดระวังความเข้าใจผิดที่พบบ่อยต่อไปนี้

  • 'การเพิ่ม HttpOnly ทำให้ผลกระทบของ XSS หมดไป.' แม้ว่า HttpOnly จะป้องกันไม่ให้เข้าถึงคุกกี้จาก JavaScript ได้ แต่การโจมตีแบบ XSS ยังสามารถถูกใช้เพื่อส่งคำขอใดๆ ก็ได้ คุณควรใช้การตรวจสอบโทเค็น CSRF, CSP (Content Security Policy) และการทำความสะอาดข้อมูลอินพุตด้วย
  • 'Secure ไม่จำเป็นสำหรับการพัฒนาในเครื่อง.' การจำลอง HTTPS และตรวจสอบพฤติกรรมแม้ในสภาพแวดล้อมในเครื่อง ช่วยให้ความแม่นยำของการทดสอบดีขึ้น อย่างน้อยควรใช้ HTTPS ในสภาพแวดล้อม staging และ production
  • 'การกำหนดเวลาหมดอายุนานๆ นั้นสะดวก.' หากตั้งอายุคุกกี้ให้นาน ระยะเวลาที่สามารถถูกนำไปใช้ในทางที่ผิดเมื่อถูกขโมยก็จะเพิ่มขึ้น คุณสามารถกำหนดเวลาหมดอายุให้สั้นลง และผนวกการยืนยันตัวตนเป็นระยะและการหมุนเวียนโทเค็น (token rotation)

สรุป

แม้ว่าคุกกี้จะใช้งานง่าย แต่การจัดการไม่ถูกต้องอาจก่อให้เกิดช่องโหว่ด้านความปลอดภัย เพื่อจัดการคุกกี้อย่างถูกต้องด้วย TypeScript สิ่งสำคัญคือการทำความเข้าใจแอตทริบิวต์อย่าง HttpOnly, Secure และ SameSite และบังคับใช้การตั้งค่าฝั่งเซิร์ฟเวอร์ที่ปลอดภัย ด้วยการไม่เก็บข้อมูลอ่อนไหวโดยตรง และผสานการลงลายเซ็นเข้ากับอายุคุกกี้ที่สั้น คุณจะสามารถจัดการเซสชันได้อย่างปลอดภัยและเชื่อถือได้

คุณสามารถติดตามบทความข้างต้นโดยใช้ Visual Studio Code บนช่อง YouTube ของเรา กรุณาตรวจสอบช่อง YouTube ด้วย

YouTube Video