TypeScript에서의 `Cookie`

TypeScript에서의 `Cookie`

이 문서는 TypeScript에서의 Cookie를 설명합니다.

브라우저와 서버 모두에서 쿠키를 안전하고 신뢰성 있게 다루는 실용적인 패턴을 살펴봅니다.

YouTube Video

TypeScript에서의 Cookie

쿠키의 기본 개념

쿠키는 클라이언트에 작은 문자열(이름=값 쌍)을 저장하는 메커니즘으로, Set-Cookie HTTP 헤더 또는 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(크로스 사이트 스크립팅) 공격을 완화하는 데 도움이 됩니다. 브라우저 측에서는 HttpOnly를 설정할 수 없다는 점에 유의하세요.
  • Secure 속성은 쿠키가 HTTPS로만 전송되도록 제한하여 도청과 변조 위험을 줄입니다.
  • SameSite 속성은 사이트 간 요청에 쿠키가 함께 전송될지 여부를 제어하여 CSRF(사이트 간 요청 위조) 공격을 방지하는 데 도움이 됩니다.
  • Path 속성은 쿠키가 전송되는 요청 경로의 범위를 지정하고, Domain 속성은 쿠키가 유효한 도메인을 지정합니다.
  • Expires 또는 Max-Age 속성을 설정하여 쿠키의 만료를 제어할 수 있습니다.

브라우저에서 SameSite/Secure를 추가하는 예(가능한 범위)

HttpOnly는 클라이언트 측에서 설정할 수 없습니다. 아래는 클라이언트 측에서 SameSiteSecure를 추가하는 예시입니다. 다만, 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)

서버에서는 Set-Cookie 헤더를 통해 HttpOnly를 추가할 수 있으므로, 세션 관리는 이상적으로 서버 측에서 수행해야 합니다. 아래는 Node의 http 모듈을 사용하는 예입니다.

 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(크로스 사이트 스크립팅) 공격이 쿠키를 탈취하기가 더 어려워집니다. 또한 쿠키가 항상 HTTPS를 통해 전송되도록 Secure 속성을 추가하면 도청과 변조를 방지하고 통신 보안을 강화할 수 있습니다.

쿠키 직렬화/파싱 구현(서버 측 헬퍼)

아래는 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 보호가 필요합니다. 일반적인 완화책은 다음과 같습니다:.

  • 불필요한 크로스 사이트 전송을 막기 위해 SameSiteLax 또는 Strict로 설정합니다. 교차 출처 통신이 필요한 경우에만 SameSite=None; Secure를 사용하세요.
  • 서버에서 검증하는 CSRF 토큰을 폼과 API 요청에 사용하세요. 토큰을 HttpOnly 쿠키에 저장하지 마세요; 대신, 응답 본문이나 메타 태그로 제공하고 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와 쿠키의 조합은 보안 관점에서 매우 까다롭습니다. 허용할 출처를 허용 목록으로 엄격히 관리하고 불필요한 교차 사이트 통신을 피하세요. 또한 SameSite=None; Secure를 사용할 때는 중간자 공격을 방지하기 위해 HTTPS를 강제하세요.

예: Express에서의 안전한 세션 쿠키 설정(TypeScript)

실무에서는 Express, cookie 라이브러리, express-session 등을 사용합니다. 아래는 expresscookie-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);
  • cookieParsersecret 기능을 사용하면 서명된 쿠키를 쉽게 다룰 수 있습니다. 그러나 실제 애플리케이션에서는 보안과 확장성 측면에서 데이터를 쿠키에 직접 저장하지 말고, 전용 세션 스토어를 사용해야 합니다.

쿠키 접두사 __Host-__Secure-

브라우저는 특정 접두사에 대해 특별한 규칙을 강제합니다.

  • __Secure- 접두사 쿠키 이름이 __Secure-로 시작하면 Secure 속성이 필수입니다.
  • __Host- 접두사 __Host-로 시작하는 경우 Secure가 필수이며, 경로는 /(루트)여야 하고 Domain은 설정하면 안 됩니다.

이들을 사용하면 오구성을 줄이고 보안을 향상시킬 수 있습니다.

쿠키 모범 사례

쿠키를 안전하게 처리하려면 다음 사항을 고려하세요.

  • 민감한 정보를 쿠키에 직접 저장하지 마십시오. 액세스 토큰에는 서버 측 세션 ID를 선호하세요.
  • 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를 사용해야 합니다.
  • '긴 만료 기간이 편리하다.' 쿠키의 수명을 길게 설정하면, 탈취되었을 때 악용될 수 있는 기간이 그만큼 늘어납니다. 만료 기간을 더 짧게 설정하고, 주기적인 재인증과 토큰 로테이션을 도입할 수 있습니다.

요약

쿠키는 사용하기 쉽지만 잘못 다루면 보안 취약점이 발생할 수 있습니다. TypeScript로 올바르게 관리하려면 HttpOnly, Secure, SameSite 같은 속성을 이해하고 안전한 서버 측 설정을 강제하는 것이 중요합니다. 민감한 데이터를 직접 저장하지 않고 서명과 짧은 만료 기간을 병행하면 안전하고 신뢰할 수 있는 세션 관리를 달성할 수 있습니다.

위의 기사를 보면서 Visual Studio Code를 사용해 우리 유튜브 채널에서 함께 따라할 수 있습니다. 유튜브 채널도 확인해 주세요.

YouTube Video