`Cookie` w TypeScript

`Cookie` w TypeScript

Ten artykuł wyjaśnia Cookie w TypeScript.

Przechodzimy przez praktyczne wzorce bezpiecznej i niezawodnej obsługi ciasteczek zarówno w przeglądarce, jak i na serwerze.

YouTube Video

Cookie w TypeScript

Podstawowe pojęcia dotyczące ciasteczek

Ciasteczko to mechanizm przechowywania na kliencie krótkich ciągów (par nazwa=wartość), tworzonych poprzez nagłówek HTTP Set-Cookie lub document.cookie. Ich zachowanie można kontrolować atrybutami bezpieczeństwa (HttpOnly, Secure, SameSite itd.).

Podstawowe operacje w przeglądarce: document.cookie

Poniżej minimalny przykład zapisania ciasteczka w przeglądarce. Utwórz ciasteczko, dopisując ciąg do 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');
  • Ten kod tworzy ciasteczko wygasające po 7 dniach. Łatwo używać tego po stronie przeglądarki, ale ponieważ nie można ustawić HttpOnly, należy unikać przechowywania poufnych informacji.

Odczytywanie ciasteczek (przeglądarka)

Poniższa funkcja to funkcja pomocnicza, która pobiera wartość ciasteczka o podanej nazwie z document.cookie. document.cookie jest zwracane jako ciąg znaków oddzielony średnikiem i spacją ('; '). Dlatego dzielimy go i szukamy szukanego ciasteczka.

 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);
  • Ta funkcja dekoduje zarówno nazwę, jak i wartość ciasteczka, aby bezpiecznie je porównać i pobrać. Obsługuje zduplikowane nazwy ciasteczek oraz zakodowane znaki specjalne, dzięki czemu jest to prosta, lecz solidna implementacja.

Zrozumienie i stosowanie atrybutów bezpieczeństwa

Pliki cookie mają kilka ważnych atrybutów, z których każdy kontroluje bezpieczeństwo i zakres działania.

  • Atrybut HttpOnly uniemożliwia dostęp do pliku cookie z poziomu JavaScriptu, co pomaga ograniczać ataki XSS (cross-site scripting). Pamiętaj, że nie można ustawić HttpOnly po stronie przeglądarki.
  • Atrybut Secure ogranicza wysyłanie plików cookie wyłącznie przez HTTPS, zmniejszając ryzyko podsłuchu i manipulacji.
  • Atrybut SameSite kontroluje, czy pliki cookie są wysyłane przy żądaniach międzywitrynowych, i pomaga zapobiegać atakom CSRF (cross-site request forgery).
  • Atrybut Path określa zakres ścieżek żądań, dla których plik cookie jest wysyłany, a atrybut Domain określa domenę, w której plik cookie obowiązuje.
  • Ustawiając atrybut Expires lub Max-Age, możesz kontrolować czas wygaśnięcia pliku cookie.

Przykład dodania SameSite / Secure w przeglądarce (to, co jest możliwe)

Atrybutu HttpOnly nie można ustawić po stronie klienta. Poniżej znajduje się przykład dodania SameSite i Secure po stronie klienta. Jednak Secure działa tylko na stronach 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 to bezpieczny i powszechny wybór. Dla silniejszych ograniczeń użyj SameSite=Strict, ale legalne nawigacje z zewnętrznych witryn mogą przestać działać.

Ustawianie ciasteczek na serwerze (Set-Cookie) (Node / TypeScript)

Na serwerze możesz dodać HttpOnly poprzez nagłówek Set-Cookie, więc zarządzanie sesją najlepiej realizować po stronie serwera. Poniżej przykład z użyciem modułu http w Node.js.

 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});
  • Gdy ustawisz atrybut HttpOnly po stronie serwera, plik cookie staje się niedostępny z poziomu JavaScriptu, co utrudnia atakom XSS (cross-site scripting) jego kradzież. Dodatkowo, dodając atrybut Secure, aby pliki cookie były zawsze wysyłane przez HTTPS, możesz zapobiegać podsłuchowi i manipulacji oraz poprawić bezpieczeństwo komunikacji.

Implementacja serializacji/parsowania ciasteczek (pomocnik po stronie serwera)

Poniżej znajduje się proste narzędzie po stronie serwera, które buduje łańcuch nagłówka 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"
  • To narzędzie stanowi podstawę tworzenia Set-Cookie na własnym serwerze. Biblioteki obsługują znacznie więcej przypadków brzegowych.

Zapobieganie manipulacjom za pomocą podpisów (HMAC)

Przechowywanie ważnych wartości bezpośrednio w ciasteczkach jest niebezpieczne. Przedstawiamy metodę podpisywania wartości na serwerze w celu wykrycia manipulacji. Tutaj używamy HMAC-SHA256. W środowisku produkcyjnym należy również zarządzać unieważnianiem tokenów po stronie serwera.

 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"
  • Dzięki tej metodzie można sprawdzić, czy wartość pliku cookie została zmanipulowana. Nie umieszczaj klucza podpisującego w kodzie źródłowym; zarządzaj nim w bezpieczny sposób, na przykład poprzez zmienne środowiskowe.

Ciasteczka i ograniczanie CSRF (ogólne wzorce)

Sesje korzystające z ciasteczek wymagają ochrony przed CSRF. Typowe środki zaradcze obejmują:.

  • Ustaw SameSite na Lax lub Strict, aby zapobiec niepotrzebnemu wysyłaniu między witrynami. Używaj SameSite=None; Secure tylko wtedy, gdy komunikacja cross-origin jest konieczna.
  • Używaj tokenów CSRF, weryfikowanych przez serwer, dla formularzy i żądań API. Nie przechowuj tokenu w pliku cookie HttpOnly; zamiast tego przekazuj go w treści odpowiedzi lub w tagach meta, a JavaScript powinien odczytywać go tylko wtedy, gdy to konieczne.
  • Jeśli token jest dostępny z poziomu JavaScriptu, upewnij się, że wdrożysz także zabezpieczenia przed XSS (CSP, escapowanie danych wyjściowych itd.).
  • Wymagaj ponownego uwierzytelnienia dla wrażliwych działań oraz przy logowaniu i zapobiegaj atakom ustalenia sesji.

Poniżej znajduje się bezpieczny przykład klienta, który wysyła token CSRF w nagłówku. Zakłada to, że klient otrzymał już token z serwera, np. w treści odpowiedzi.

 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}
  • Jeśli określisz credentials: 'same-origin', wysyłane będą tylko pliki cookie dla tego samego originu. Po stronie serwera zweryfikuj wartość nagłówka X-CSRF-Token i sprawdź, czy token jest zgodny. Jeśli żądania cross-site są konieczne, ostrożnie skonfiguruj CORS.

Międzywitrynowość (CORS) i związek z credentials

Aby wysyłać i odbierać pliki cookie w komunikacji cross-origin, klient musi ustawić credentials: 'include', a serwer musi ustawić Access-Control-Allow-Credentials: true. Jednak ogranicz tę konfigurację do zaufanych originów i nie używaj 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}
  • Połączenie CORS i plików cookie jest bardzo delikatne z punktu widzenia bezpieczeństwa. Zarządzaj dozwolonymi originami rygorystycznie za pomocą białej listy i unikaj niepotrzebnej komunikacji cross-site. Dodatkowo, używając SameSite=None; Secure, wymuszaj HTTPS, aby zapobiegać atakom typu man-in-the-middle.

Przykład: bezpieczne ustawienia ciasteczek sesyjnych w Express (TypeScript)

W praktyce użyj Express, biblioteki cookie, express-session itd. Poniżej prosty przykład z użyciem express i cookie-parser. W praktyce ustaw secure na true i zarządzaj secret poprzez zmienne środowiskowe.

 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);
  • Korzystając z funkcji secret w cookieParser, możesz łatwo pracować z podpisanymi ciasteczkami. Jednak w rzeczywistych aplikacjach, z perspektywy bezpieczeństwa i skalowalności, nie należy przechowywać danych bezpośrednio w ciasteczkach; zamiast tego użyj dedykowanego magazynu sesji.

Prefiksy ciasteczek __Host- i __Secure-

Przeglądarki wymuszają szczególne zasady dla niektórych prefiksów.

  • Prefiks __Secure- Jeśli nazwa ciasteczka zaczyna się od __Secure-, wymagany jest atrybut Secure.
  • Prefiks __Host- Jeśli zaczyna się od __Host-, wymagany jest Secure, ścieżka musi wynosić / (root), a Domain nie może być ustawiony.

Ich użycie zmniejsza ryzyko błędnej konfiguracji i poprawia bezpieczeństwo.

Najlepsze praktyki dotyczące ciasteczek

Aby bezpiecznie obsługiwać pliki cookie, weź pod uwagę następujące kwestie.

  • Nie przechowuj bezpośrednio w ciasteczkach wrażliwych informacji. Preferuj po stronie serwera identyfikator sesji dla tokenów dostępu.
  • Ustawiaj ciasteczka sesyjne z HttpOnly, Secure i SameSite=Lax (lub Strict).
  • Wykorzystuj prefiksy takie jak __Host- i __Secure-.
  • Rozważ podpisy (HMAC) i szyfrowanie, aby zapobiegać manipulacji i podsłuchowi.
  • Włącz Secure i wymagaj HTTPS.
  • Dąż do zasady najmniejszych uprawnień i krótkich okresów ważności.
  • Używaj tokenów CSRF.
  • Uważaj na różnice w działaniu SameSite w różnych przeglądarkach, zwłaszcza starszych.

Powszechne nieporozumienia dotyczące ciasteczek

W odniesieniu do plików cookie zwróć uwagę na następujące powszechne błędne przekonania.

  • 'Dodanie HttpOnly eliminuje wpływ XSS.' Choć HttpOnly uniemożliwia dostęp do plików cookie z poziomu JavaScriptu, XSS nadal może zostać użyty do wysyłania dowolnych żądań. Należy również stosować weryfikację tokenów CSRF, CSP (Content Security Policy) oraz oczyszczanie danych wejściowych.
  • 'Secure jest niepotrzebny w lokalnym środowisku.' Symulowanie HTTPS i weryfikowanie zachowania nawet w środowiskach lokalnych poprawia dokładność testów. Co najmniej w środowiskach staging i produkcyjnych należy używać HTTPS.
  • 'Długie okresy ważności są wygodne.' Jeśli ustawisz długi czas życia plików cookie, wydłuża się okres, w którym po kradzieży mogą zostać nadużyte. Możesz ustawić krótsze czasy wygaśnięcia oraz wprowadzić okresowe ponowne uwierzytelnianie i rotację tokenów.

Podsumowanie

Choć ciasteczka są łatwe w użyciu, niewłaściwe obchodzenie się z nimi może wprowadzać luki bezpieczeństwa. Aby właściwie nimi zarządzać w TypeScript, ważne jest zrozumienie atrybutów takich jak HttpOnly, Secure i SameSite oraz egzekwowanie bezpiecznych ustawień po stronie serwera. Nie przechowując bezpośrednio danych wrażliwych i łącząc podpisy z krótkimi okresami ważności, można osiągnąć bezpieczne i niezawodne zarządzanie sesjami.

Możesz śledzić ten artykuł, korzystając z Visual Studio Code na naszym kanale YouTube. Proszę również sprawdzić nasz kanał YouTube.

YouTube Video