Ver3.0 ๐ก๏ธ CSRF ๋ฐฉ์ด ํ ํฐ ์์ฑ ์์ค์ฝ๋ ์๋ฒฝ ๊ฐ์ด๋

๐ก๏ธ CSRF ๋ฐฉ์ด ํ ํฐ ์์ฑ ์์ค์ฝ๋ ์๋ฒฝ ๊ฐ์ด๋
์น ๋ณด์์ ํ์ํ ! CSRF ๊ณต๊ฒฉ ๋ง๋ ํ ํฐ ์์ฑ ๋ฐฉ๋ฒ ์ด์ ๋ฆฌ ใ ใ ใ
๐ฏ CSRF๊ฐ ๋ญ๋ฐ ์ด๋ ๊ฒ ๋๋ฆฌ์ผ?
์ผ ์์งํ ๋งํด์ ์น ๊ฐ๋ฐํ๋ค๋ณด๋ฉด ๋ณด์ ์ด์ ์ง์ง ๊ฐ๋ง์์ ใ
ใ
ใ
๊ทธ ์ค์์๋ CSRF(Cross-Site Request Forgery)๋ ์ง์ง ๋ฌด์ํ๋ฉด ํฐ์ผ๋๋ ๋
์์ด์ผ.
๊ฐ๋จํ๊ฒ ์ค๋ช
ํ๋ฉด, ์ฌ์ฉ์๊ฐ ์๋ํ์ง ์์ ์์ฒญ์ ๊ณต๊ฒฉ์๊ฐ ๋ชฐ๋ ๋ณด๋ด๊ฒ ๋ง๋๋ ๊ณต๊ฒฉ ๋ฐฉ์์ด๊ฑฐ๋ ? ์๋ฅผ ๋ค์ด ๋ค๊ฐ ์ํ ์ฌ์ดํธ์ ๋ก๊ทธ์ธํ ์ํ์์ ์
์ฑ ๋งํฌ๋ฅผ ํด๋ฆญํ๋ฉด, ๋๋ ๋ชจ๋ฅด๊ฒ ๋์ด ์ด์ฒด๋ ์ ์๋ค๋ ๊ฑฐ์ง ใทใทใท
์ด๋ฐ ๊ณต๊ฒฉ์ ๋ง๊ธฐ ์ํด ๋์จ ๊ฒ ๋ฐ๋ก CSRF ํ ํฐ์ด์ผ! ํ ํฐ์ ์ผ์ข
์ ๋น๋ฐ ์ํธ ๊ฐ์ ๊ฑด๋ฐ, ์๋ฒ๊ฐ ์์ฑํด์ ํด๋ผ์ด์ธํธ์๊ฒ ์ฃผ๊ณ , ํด๋ผ์ด์ธํธ๋ ์์ฒญํ ๋๋ง๋ค ์ด ํ ํฐ์ ๊ฐ์ด ๋ณด๋ด๋ ๊ฑฐ์ง. ์๋ฒ๋ ์ด ํ ํฐ์ ๊ฒ์ฆํด์ "์ ์ด๊ฑด ์ง์ง ์ฐ๋ฆฌ ์ฌ์ฉ์๊ฐ ๋ณด๋ธ ์์ฒญ์ด๊ตฌ๋" ํ๊ณ ํ์ธํ๋ ๊ฑฐ์ผ.
2. ๊ณต๊ฒฉ์๊ฐ ๋ง๋ ์ ์ฑ ์ฌ์ดํธ B๋ฅผ ๋ฐฉ๋ฌธ
3. ์ฌ์ดํธ B์ ์จ๊ฒจ์ง ํผ์ด ์๋์ผ๋ก A์ํ์ ์ก๊ธ ์์ฒญ ์ ์ก
4. ๋ธ๋ผ์ฐ์ ๊ฐ ์๋์ผ๋ก A์ํ ์ฟ ํค๋ฅผ ํจ๊ป ์ ์ก
5. A์ํ ์๋ฒ๋ ์ ์ ์์ฒญ์ผ๋ก ์ฐฉ๊ฐํ๊ณ ์ก๊ธ ์ฒ๋ฆฌ ๐ฑ
๐ CSRF ํ ํฐ์ ํต์ฌ ์๋ฆฌ
CSRF ํ ํฐ์ด ์๋ํ๋ ์๋ฆฌ๋ ์๊ฐ๋ณด๋ค ์ฌํํด ใ
ใ
๊ทผ๋ฐ ์ด๊ฒ ์ง์ง ํจ๊ณผ์ ์ด๊ฑฐ๋ ?
๊ธฐ๋ณธ ์์ด๋์ด๋ ์ด๊ฑฐ์ผ: ์์ธก ๋ถ๊ฐ๋ฅํ ๋๋ค ๊ฐ์ ์์ฑํด์ ์๋ฒ์ ํด๋ผ์ด์ธํธ๋ง ๊ณต์ ํ๋ ๊ฑฐ์ง. ๊ณต๊ฒฉ์๋ ์ด ๊ฐ์ ์ ์ ์์ผ๋๊น ์ ์์ ์ธ ์์ฒญ์ ์์กฐํ ์ ์๊ฒ ๋๋ ์๋ฆฌ์ผ.
ํ ํฐ์ ๋ณดํต ์ธ์
๋ง๋ค ๋๋ ์์ฒญ๋ง๋ค ์๋ก ์์ฑ๋๊ณ , ์๋ฒ ์ธก์์๋ ์ด ํ ํฐ์ ์ธ์
์ด๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ ์ ์ฅํด๋ฌ. ๊ทธ๋ฆฌ๊ณ ํด๋ผ์ด์ธํธ๊ฐ ํผ์ ์ ์ถํ๊ฑฐ๋ API ์์ฒญ์ ๋ณด๋ผ ๋ ์ด ํ ํฐ์ ํจ๊ป ๋ณด๋ด๋ฉด, ์๋ฒ๋ ์ ์ฅ๋ ํ ํฐ๊ณผ ๋น๊ตํด์ ๊ฒ์ฆํ๋ ๊ฑฐ์ง.
2. ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ๊ธฐ: Math.random() ๊ฐ์ ๊ฑฐ ์ฐ๋ฉด ์ ๋ผ! crypto ๋ชจ๋ ์จ์ผ ํจ.
3. ํ ํฐ ์ ํจ๊ธฐ๊ฐ: ๋๋ฌด ๊ธธ๋ฉด ์ํํ๊ณ , ๋๋ฌด ์งง์ผ๋ฉด ์ฌ์ฉ์ ๊ฒฝํ์ด ๋๋น ์ ธ. ๋ณดํต ์ธ์ ํ์์์๊ณผ ๋์ผํ๊ฒ ์ค์ .
4. ํ ํฐ ์ ์ฅ ์์น: ์๋ฒ ์ธก ์ธ์ , ๋ฐ์ดํฐ๋ฒ ์ด์ค, ๋๋ ์ํธํ๋ ์ฟ ํค์ ์ ์ฅ ๊ฐ๋ฅ.
๐ป Node.js + Express์์ CSRF ํ ํฐ ์์ฑํ๊ธฐ
์ ์ด์ ๋ณธ๊ฒฉ์ ์ผ๋ก ์ฝ๋ ๋ค์ด๊ฐ๋ค ใ
ใ
ใ
Node.js ํ๊ฒฝ์์ ๊ฐ์ฅ ๋ง์ด ์ฐ๋ ๋ฐฉ๋ฒ๋ถํฐ ๋ณผ๊ฒ.
Express์์๋ csurf ๋ฏธ๋ค์จ์ด๊ฐ ์ ํต์ ์ผ๋ก ๋ง์ด ์ฐ์๋๋ฐ, ์ต๊ทผ์๋ ๋ ํ๋์ ์ธ ๋ฐฉ๋ฒ๋ค๋ ๋ง์ด ๋์์ด. ๊ทผ๋ฐ ์๋ฆฌ๋ฅผ ์ดํดํ๋ ค๋ฉด ์ง์ ๊ตฌํํด๋ณด๋ ๊ฒ ์ต๊ณ ์ง!
const crypto = require('crypto');
// CSRF ํ ํฐ ์์ฑ ํจ์
function generateCSRFToken() {
// 32๋ฐ์ดํธ(256๋นํธ)์ ๋๋ค ๋ฐ์ดํฐ ์์ฑ
return crypto.randomBytes(32).toString('hex');
}
// ์ฌ์ฉ ์์
const token = generateCSRFToken();
console.log('์์ฑ๋ ํ ํฐ:', token);
// ์ถ๋ ฅ ์: ์์ฑ๋ ํ ํฐ: a3f5e8d9c2b1a4f6e7d8c9b0a1f2e3d4c5b6a7f8e9d0c1b2a3f4e5d6c7b8a9f0
์ด๊ฒ ๊ฐ์ฅ ๊ธฐ๋ณธ์ ์ธ ํํ์ผ. crypto.randomBytes()๋ ์ํธํ์ ์ผ๋ก ์์ ํ ๋์๋ฅผ ์์ฑํด์ฃผ๊ฑฐ๋ . ๊ทธ๋ฆฌ๊ณ hex๋ก ์ธ์ฝ๋ฉํ๋ฉด ๋ฌธ์์ด๋ก ๋ณํ๋ผ์ ๋ค๋ฃจ๊ธฐ ํธํด์ง ใ ใ
const express = require('express');
const session = require('express-session');
const crypto = require('crypto');
const app = express();
// ์ธ์
์ค์
app.use(session({
secret: 'your-secret-key-here',
resave: false,
saveUninitialized: true,
cookie: {
secure: true, // HTTPS์์๋ง ์๋
httpOnly: true, // JavaScript ์ ๊ทผ ์ฐจ๋จ
sameSite: 'strict' // CSRF ์ถ๊ฐ ๋ฐฉ์ด
}
}));
// CSRF ํ ํฐ ์์ฑ ๋ฏธ๋ค์จ์ด
app.use((req, res, next) => {
if (!req.session.csrfToken) {
req.session.csrfToken = crypto.randomBytes(32).toString('hex');
}
// ํ
ํ๋ฆฟ์์ ์ฌ์ฉํ ์ ์๋๋ก locals์ ์ ์ฅ
res.locals.csrfToken = req.session.csrfToken;
next();
});
// CSRF ํ ํฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function verifyCSRFToken(req, res, next) {
const token = req.body.csrfToken || req.headers['x-csrf-token'];
if (!token || token !== req.session.csrfToken) {
return res.status(403).json({
error: 'CSRF ํ ํฐ์ด ์ ํจํ์ง ์์ต๋๋ค ใ
ใ
'
});
}
next();
}
// ํผ ํ์ด์ง ๋ผ์ฐํธ
app.get('/form', (req, res) => {
res.send(`
<form method="POST" action="/submit">
<input type="hidden" name="csrfToken" value="${res.locals.csrfToken}">
<input type="text" name="data" placeholder="๋ฐ์ดํฐ ์
๋ ฅ">
<button type="submit">์ ์ก</button>
</form>
`);
});
// ํผ ์ ์ถ ์ฒ๋ฆฌ (CSRF ๋ณดํธ๋จ)
app.post('/submit', verifyCSRFToken, (req, res) => {
res.json({
success: true,
message: '๋ฐ์ดํฐ๊ฐ ์์ ํ๊ฒ ์ฒ๋ฆฌ๋์์ต๋๋ค! ๐'
});
});
app.listen(3000, () => {
console.log('์๋ฒ ์คํ ์ค: http://localhost:3000');
});
- HTTPS ํ์ ์ฌ์ฉ
- ์ธ์ ์คํ ์ด๋ฅผ Redis ๊ฐ์ ๊ณณ์ ์ ์ฅ
- ํ ํฐ ๋ง๋ฃ ์๊ฐ ์ค์
- Rate limiting ์ ์ฉ
- ์๋ฌ ํธ๋ค๋ง ๊ฐํ
๐จ csurf ํจํค์ง ์ฌ์ฉํ๊ธฐ
์ง์ ๊ตฌํํ๋ ๊ฒ๋ ์ข์ง๋ง, ๊ฒ์ฆ๋ ๋ผ์ด๋ธ๋ฌ๋ฆฌ๋ฅผ ์ฐ๋ ๊ฒ ๋ ์์ ํ ๋๊ฐ ๋ง์ ใ ใ csurf๋ Express์ฉ CSRF ๋ณดํธ ๋ฏธ๋ค์จ์ด์ธ๋ฐ, ์ฌ์ฉ๋ฒ์ด ์ง์ง ๊ฐ๋จํด.
// ์ค์น
// npm install csurf cookie-parser
const express = require('express');
const cookieParser = require('cookie-parser');
const csrf = require('csurf');
const app = express();
// ๋ฏธ๋ค์จ์ด ์ค์
app.use(cookieParser());
app.use(express.urlencoded({ extended: false }));
// CSRF ๋ณดํธ ์ค์ (์ฟ ํค ๊ธฐ๋ฐ)
const csrfProtection = csrf({ cookie: true });
// ๋ชจ๋ ๋ผ์ฐํธ์ ์ ์ฉ
app.use(csrfProtection);
// ํผ ๋ ๋๋ง
app.get('/form', (req, res) => {
// req.csrfToken()์ผ๋ก ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
res.send(`
<form method="POST" action="/process">
<input type="hidden" name="_csrf" value="${req.csrfToken()}">
<input type="text" name="username" placeholder="์ฌ์ฉ์๋ช
">
<button type="submit">์ ์ถ</button>
</form>
`);
});
// POST ์์ฒญ ์ฒ๋ฆฌ (์๋์ผ๋ก CSRF ๊ฒ์ฆ๋จ)
app.post('/process', (req, res) => {
res.send('์ฒ๋ฆฌ ์๋ฃ! โ
');
});
// CSRF ์๋ฌ ํธ๋ค๋ง
app.use((err, req, res, next) => {
if (err.code === 'EBADCSRFTOKEN') {
res.status(403).send('CSRF ํ ํฐ ๊ฒ์ฆ ์คํจ ๐ข');
} else {
next(err);
}
});
app.listen(3000);
csurf์ ์ฅ์ ์ ์๋์ผ๋ก ํ ํฐ ์์ฑ, ๊ฒ์ฆ, ์๋ฌ ์ฒ๋ฆฌ๋ฅผ ํด์ค๋ค๋ ๊ฑฐ์ผ. ๊ทผ๋ฐ ์ต๊ทผ์๋ ์ด ํจํค์ง๊ฐ deprecated ๋์ด ใ ใ ๊ทธ๋์ ๋์์ ์ฐพ์์ผ ํ๋๋ฐ, ๊ทธ๊ฑด ์กฐ๊ธ ์๋ค๊ฐ ์ค๋ช ํ ๊ฒ!
๐ Python Flask์์ CSRF ํ ํฐ ๊ตฌํ
Python ๊ฐ๋ฐ์๋ค ์ฌ๊ธฐ ์ฃผ๋ชฉ ใ ใ ใ Flask์์๋ CSRF ๋ณดํธ๋ ํ์์ผ. Flask-WTF ํ์ฅ์ ์ฐ๋ฉด ์ง์ง ์ฝ๊ฒ ๊ตฌํํ ์ ์์ด.
from flask import Flask, render_template, request, jsonify
from flask_wtf.csrf import CSRFProtect, generate_csrf
import secrets
app = Flask(__name__)
# ์ํฌ๋ฆฟ ํค ์ค์ (์ค์ ๋ก๋ ํ๊ฒฝ๋ณ์์์ ๊ฐ์ ธ์์ผ ํจ)
app.config['SECRET_KEY'] = secrets.token_hex(32)
# CSRF ๋ณดํธ ํ์ฑํ
csrf = CSRFProtect(app)
@app.route('/form')
def form():
# ํ
ํ๋ฆฟ์ ์๋์ผ๋ก csrf_token() ํจ์ ์ ๊ณต๋จ
return '''
<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<input type="text" name="data">
<button type="submit">์ ์ก</button>
</form>
'''
@app.route('/submit', methods=['POST'])
def submit():
# CSRF ํ ํฐ์ ์๋์ผ๋ก ๊ฒ์ฆ๋จ
data = request.form.get('data')
return jsonify({'success': True, 'data': data})
# API ์๋ํฌ์ธํธ์์ CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
@app.route('/api/get-token')
def get_token():
token = generate_csrf()
return jsonify({'csrfToken': token})
# ํน์ ๋ผ์ฐํธ์์ CSRF ๋ณดํธ ๋นํ์ฑํ (API ๋ฑ)
@app.route('/api/public', methods=['POST'])
@csrf.exempt
def public_api():
return jsonify({'message': '๊ณต๊ฐ API ์๋ํฌ์ธํธ'})
if __name__ == '__main__':
app.run(debug=True)
from flask import Flask, session
import secrets
import hashlib
import hmac
app = Flask(__name__)
app.secret_key = secrets.token_hex(32)
def generate_csrf_token():
"""CSRF ํ ํฐ ์์ฑ"""
if '_csrf_token' not in session:
session['_csrf_token'] = secrets.token_urlsafe(32)
return session['_csrf_token']
def validate_csrf_token(token):
"""CSRF ํ ํฐ ๊ฒ์ฆ"""
return token == session.get('_csrf_token')
# ํ
ํ๋ฆฟ์์ ์ฌ์ฉํ ์ ์๋๋ก ์ปจํ
์คํธ ํ๋ก์ธ์ ๋ฑ๋ก
@app.context_processor
def inject_csrf_token():
return dict(csrf_token=generate_csrf_token)
@app.before_request
def csrf_protect():
"""POST ์์ฒญ์ ๋ํด CSRF ํ ํฐ ๊ฒ์ฆ"""
if request.method == "POST":
token = request.form.get('_csrf_token')
if not token or not validate_csrf_token(token):
abort(403, 'CSRF ํ ํฐ ๊ฒ์ฆ ์คํจ!')
# ์ฌ์ฉ ์์
@app.route('/protected-form')
def protected_form():
return render_template('form.html')
Flask๋ ์ง์ง ๊ฐ๋จํ์ง? ใ ใ Flask-WTF๊ฐ ๋๋ถ๋ถ์ ์์ ์ ์๋์ผ๋ก ์ฒ๋ฆฌํด์ค์ ๊ฐ๋ฐ์๋ ์ ๊ฒฝ ์ธ ๊ฒ ๋ณ๋ก ์์ด. ๊ทผ๋ฐ ๋ด๋ถ ๋์ ์๋ฆฌ๋ฅผ ์ดํดํ๋ ๊ฑด ์ค์ํ๋๊น ์๋ ๊ตฌํ ์ฝ๋๋ ๊ฐ์ด ๋ด๋ฌ!
โ Java Spring์์ CSRF ํ ํฐ ์ฒ๋ฆฌ
์ํฐํ๋ผ์ด์ฆ ํ๊ฒฝ์์ ๋ง์ด ์ฐ๋ Spring๋ ๋นผ๋์ ์ ์์ง ใ ใ Spring Security๋ ๊ธฐ๋ณธ์ ์ผ๋ก CSRF ๋ณดํธ๊ฐ ํ์ฑํ๋์ด ์์ด. ์คํ๋ ค ๋นํ์ฑํํ๋ ๊ฒ ๋ ์ด๋ ค์ธ ์ ๋ ใ ใ ใ
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.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
// CSRF ๋ณดํธ ํ์ฑํ (๊ธฐ๋ณธ๊ฐ)
.csrf(csrf -> csrf
// ์ฟ ํค ๊ธฐ๋ฐ ํ ํฐ ์ ์ฅ์ ์ฌ์ฉ
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// ํน์ ๊ฒฝ๋ก๋ CSRF ๋ณดํธ ์ ์ธ
.ignoringRequestMatchers("/api/public/**")
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}
<!-- Thymeleaf๋ ์๋์ผ๋ก CSRF ํ ํฐ์ ํผ์ ์ถ๊ฐํจ -->
<form th:action="@{/submit}" method="post">
<!-- ์ด ๋ถ๋ถ์ด ์๋์ผ๋ก ์ถ๊ฐ๋จ:
<input type="hidden" name="_csrf" value="ํ ํฐ๊ฐ"/>
-->
<input type="text" name="data"/>
<button type="submit">์ ์ก</button>
</form>
<!-- AJAX ์์ฒญ์์ CSRF ํ ํฐ ์ฌ์ฉ -->
<script th:inline="javascript">
const token = /*[[${_csrf.token}]]*/ '';
const header = /*[[${_csrf.headerName}]]*/ '';
fetch('/api/data', {
method: 'POST',
headers: {
[header]: token,
'Content-Type': 'application/json'
},
body: JSON.stringify({ data: 'test' })
});
</script>
import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.web.bind.annotation.*;
@RestController
public class CsrfController {
// CSRF ํ ํฐ ์ ๋ณด ๋ฐํ
@GetMapping("/api/csrf-token")
public CsrfToken getCsrfToken(CsrfToken token) {
return token;
}
// POST ์์ฒญ ์ฒ๋ฆฌ (์๋์ผ๋ก CSRF ๊ฒ์ฆ๋จ)
@PostMapping("/api/submit")
public ResponseEntity<String> submitData(@RequestBody DataDto data) {
// CSRF ํ ํฐ์ด ์ ํจํ๋ฉด ์ด ๋ฉ์๋๊ฐ ์คํ๋จ
return ResponseEntity.ok("๋ฐ์ดํฐ ์ฒ๋ฆฌ ์๋ฃ! ๐");
}
}
Spring์ ์ฅ์ ์ ํ๋ ์์ํฌ ๋ ๋ฒจ์์ CSRF ๋ณดํธ๋ฅผ ์ ๊ณตํ๋ค๋ ๊ฑฐ์ผ. ๊ฐ๋ฐ์๊ฐ ๋ณ๋๋ก ๊ตฌํํ ํ์ ์์ด ์ค์ ๋ง ํด์ฃผ๋ฉด ๋! ๊ทผ๋ฐ SPA(Single Page Application)๋ REST API๋ฅผ ๋ง๋ค ๋๋ ์ค์ ์ ์ข ๋ ์ธ๋ฐํ๊ฒ ์กฐ์ ํด์ผ ํด.
โ๏ธ React + REST API์์ CSRF ํ ํฐ ์ฒ๋ฆฌ
์์ฆ ๋์ธ์ธ SPA ํ๊ฒฝ์์๋ CSRF ํ ํฐ ์ฒ๋ฆฌ๊ฐ ์ข ๋ฌ๋ผ ใ
ใ
์ ํต์ ์ธ ํผ ์ ์ถ ๋ฐฉ์์ด ์๋๋ผ AJAX/Fetch๋ก ํต์ ํ๋๊น ๋ง์ด์ผ.
์ฌ๋ฅ๋ท ๊ฐ์ ํ๋ซํผ์์๋ ํ๋ก ํธ์๋ ๊ฐ๋ฐ ์ฌ๋ฅ์ ๊ฑฐ๋ํ ๋ ์ด๋ฐ ๋ณด์ ์ง์์ด ์์ผ๋ฉด ์ง์ง ์ ๋ฆฌํ๊ฑฐ๋ ? ํด๋ผ์ด์ธํธ ์ธก ๋ณด์ ๊ตฌํ ๋ฅ๋ ฅ์ ํ์์ผ ใ
ใ
// csrfToken.js - CSRF ํ ํฐ ์ ํธ๋ฆฌํฐ
export const getCsrfToken = async () => {
try {
const response = await fetch('/api/csrf-token', {
credentials: 'include' // ์ฟ ํค ํฌํจ
});
const data = await response.json();
return data.csrfToken;
} catch (error) {
console.error('CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ ์คํจ ใ
ใ
', error);
return null;
}
};
// ์ฟ ํค์์ CSRF ํ ํฐ ์ฝ๊ธฐ
export const getCsrfTokenFromCookie = () => {
const name = 'XSRF-TOKEN=';
const decodedCookie = decodeURIComponent(document.cookie);
const cookies = decodedCookie.split(';');
for (let cookie of cookies) {
cookie = cookie.trim();
if (cookie.indexOf(name) === 0) {
return cookie.substring(name.length);
}
}
return null;
};
// API ํธ์ถ ๋ํผ ํจ์
export const apiCall = async (url, options = {}) => {
const token = getCsrfTokenFromCookie() || await getCsrfToken();
const defaultOptions = {
credentials: 'include',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
...options.headers
}
};
return fetch(url, { ...defaultOptions, ...options });
};
import React, { useState, useEffect } from 'react';
import { apiCall, getCsrfToken } from './csrfToken';
function ProtectedForm() {
const [csrfToken, setCsrfToken] = useState('');
const [formData, setFormData] = useState({ name: '', email: '' });
const [message, setMessage] = useState('');
// ์ปดํฌ๋ํธ ๋ง์ดํธ ์ CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
useEffect(() => {
const fetchToken = async () => {
const token = await getCsrfToken();
setCsrfToken(token);
};
fetchToken();
}, []);
const handleSubmit = async (e) => {
e.preventDefault();
try {
const response = await apiCall('/api/submit', {
method: 'POST',
body: JSON.stringify(formData)
});
if (response.ok) {
const result = await response.json();
setMessage('์ ์ก ์ฑ๊ณต! ๐');
} else {
setMessage('์ ์ก ์คํจ ๐ข');
}
} catch (error) {
console.error('์๋ฌ ๋ฐ์:', error);
setMessage('๋คํธ์ํฌ ์ค๋ฅ ๋ฐ์ ใ
ใ
');
}
};
return (
<div>
<form onSubmit={handleSubmit}>
<input
type="text"
value={formData.name}
onChange={(e) => setFormData({...formData, name: e.target.value})}
placeholder="์ด๋ฆ"
/>
<input
type="email"
value={formData.email}
onChange={(e) => setFormData({...formData, email: e.target.value})}
placeholder="์ด๋ฉ์ผ"
/>
<button type="submit">์ ์ก</button>
</form>
{message && <p>{message}</p>}
</div>
);
}
export default ProtectedForm;
import axios from 'axios';
// Axios ์ธ์คํด์ค ์์ฑ
const api = axios.create({
baseURL: '/api',
withCredentials: true // ์ฟ ํค ์๋ ํฌํจ
});
// ์์ฒญ ์ธํฐ์
ํฐ: ๋ชจ๋ ์์ฒญ์ CSRF ํ ํฐ ์๋ ์ถ๊ฐ
api.interceptors.request.use(
async (config) => {
// ์ฟ ํค์์ ํ ํฐ ์ฝ๊ธฐ
const token = document.cookie
.split('; ')
.find(row => row.startsWith('XSRF-TOKEN='))
?.split('=')[1];
if (token) {
config.headers['X-CSRF-Token'] = token;
}
return config;
},
(error) => {
return Promise.reject(error);
}
);
// ์๋ต ์ธํฐ์
ํฐ: CSRF ์๋ฌ ์ฒ๋ฆฌ
api.interceptors.response.use(
(response) => response,
async (error) => {
if (error.response?.status === 403) {
console.error('CSRF ํ ํฐ ๊ฒ์ฆ ์คํจ! ํ ํฐ์ ๊ฐฑ์ ํด์ผ ํจ ใ
ใ
');
// ํ ํฐ ๊ฐฑ์ ๋ก์ง ์ถ๊ฐ ๊ฐ๋ฅ
}
return Promise.reject(error);
}
);
// ์ฌ์ฉ ์์
export const submitData = async (data) => {
try {
const response = await api.post('/submit', data);
return response.data;
} catch (error) {
console.error('API ํธ์ถ ์คํจ:', error);
throw error;
}
};
export default api;
Axios ์ธํฐ์ ํฐ๋ฅผ ์ฐ๋ฉด ์ง์ง ํธํด ใ ใ ใ ๋ชจ๋ ์์ฒญ์ ์๋์ผ๋ก CSRF ํ ํฐ์ด ๋ถ์ผ๋๊น ์ผ์ผ์ด ์ ๊ฒฝ ์ธ ํ์๊ฐ ์๊ฑฐ๋ . ์ด๋ฐ ํจํด์ ๋๊ท๋ชจ ํ๋ก์ ํธ์์ ํ์์ผ!
๐ Double Submit Cookie ํจํด
์ธ์
์ ์ฌ์ฉํ์ง ์๋ stateless API์์๋ ์ด๋ป๊ฒ CSRF๋ฅผ ๋ง์๊น? ์ฌ๊ธฐ์ ๋ฑ์ฅํ๋ ๊ฒ Double Submit Cookie ํจํด์ด์ผ ใ
ใ
์ด ๋ฐฉ์์ ์๋ฒ๊ฐ ํ ํฐ์ ์ ์ฅํ์ง ์๊ณ ๋ CSRF๋ฅผ ๋ฐฉ์ดํ ์ ์์ด. ์๋ฆฌ๋ ๊ฐ๋จํด: ๊ฐ์ ํ ํฐ์ ์ฟ ํค์ ์์ฒญ ํค๋(๋๋ ๋ฐ๋) ์์ชฝ์ ๋ณด๋ด๋ ๊ฑฐ์ง. ๊ณต๊ฒฉ์๋ ์ฟ ํค๋ฅผ ์ฝ์ ์ ์์ผ๋๊น ํค๋์ ์ฌ๋ฐ๋ฅธ ํ ํฐ์ ๋ฃ์ ์ ์๋ค๋ ๊ฒ ํต์ฌ!
const express = require('express');
const cookieParser = require('cookie-parser');
const crypto = require('crypto');
const app = express();
app.use(cookieParser());
app.use(express.json());
// CSRF ํ ํฐ ์์ฑ ๋ฐ ์ฟ ํค ์ค์
app.use((req, res, next) => {
if (!req.cookies.csrfToken) {
const token = crypto.randomBytes(32).toString('hex');
res.cookie('csrfToken', token, {
httpOnly: false, // JavaScript์์ ์ฝ์ ์ ์์ด์ผ ํจ!
secure: true,
sameSite: 'strict',
maxAge: 3600000 // 1์๊ฐ
});
}
next();
});
// CSRF ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function verifyDoubleSubmitToken(req, res, next) {
const cookieToken = req.cookies.csrfToken;
const headerToken = req.headers['x-csrf-token'];
// ์ฟ ํค์ ํค๋์ ํ ํฐ์ด ์ผ์นํ๋์ง ํ์ธ
if (!cookieToken || !headerToken || cookieToken !== headerToken) {
return res.status(403).json({
error: 'CSRF ํ ํฐ ๋ถ์ผ์น! ๊ณต๊ฒฉ ์๋ ๊ฐ์ง ๐จ'
});
}
next();
}
// ํ ํฐ ๋ฐ๊ธ ์๋ํฌ์ธํธ
app.get('/api/token', (req, res) => {
res.json({
csrfToken: req.cookies.csrfToken,
message: 'ํ ํฐ ๋ฐ๊ธ ์๋ฃ โ
'
});
});
// ๋ณดํธ๋ API ์๋ํฌ์ธํธ
app.post('/api/protected', verifyDoubleSubmitToken, (req, res) => {
res.json({
success: true,
message: '์์ฒญ์ด ์์ ํ๊ฒ ์ฒ๋ฆฌ๋์์ต๋๋ค! ๐',
data: req.body
});
});
app.listen(3000);
// ์ฟ ํค์์ CSRF ํ ํฐ ์ฝ๊ธฐ
function getCookie(name) {
const value = `; ${document.cookie}`;
const parts = value.split(`; ${name}=`);
if (parts.length === 2) {
return parts.pop().split(';').shift();
}
return null;
}
// API ํธ์ถ ํจ์
async function makeProtectedRequest(data) {
const csrfToken = getCookie('csrfToken');
if (!csrfToken) {
console.error('CSRF ํ ํฐ์ด ์์ ใ
ใ
');
return;
}
try {
const response = await fetch('/api/protected', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken // ์ฟ ํค ๊ฐ์ ํค๋์๋ ์ ์ก
},
credentials: 'include', // ์ฟ ํค ํฌํจ
body: JSON.stringify(data)
});
const result = await response.json();
console.log('์๋ต:', result);
return result;
} catch (error) {
console.error('์์ฒญ ์คํจ:', error);
}
}
// ์ฌ์ฉ ์์
makeProtectedRequest({
username: 'testuser',
action: 'update_profile'
});
โ ์๋ฒ์์ ํ ํฐ์ ์ ์ฅํ ํ์ ์์ (stateless)
โ ์ธ์ ๊ด๋ฆฌ ๋ถํ์
โ ํ์ฅ์ฑ์ด ์ข์ (์ฌ๋ฌ ์๋ฒ์์ ๋์ ๊ฐ๋ฅ)
โ ๊ตฌํ์ด ๋น๊ต์ ๊ฐ๋จ
๋จ์ :
โ ์๋ธ๋๋ฉ์ธ ๊ณต๊ฒฉ์ ์ทจ์ฝํ ์ ์์
โ httpOnly ์ฟ ํค๋ฅผ ์ฌ์ฉํ ์ ์์
โ XSS ๊ณต๊ฒฉ์ ๋ ธ์ถ๋๋ฉด ํ ํฐ์ด ํ์ทจ๋ ์ ์์
๐ญ HMAC ๊ธฐ๋ฐ CSRF ํ ํฐ
์ข ๋ ๊ณ ๊ธ ๊ธฐ๋ฒ์ผ๋ก ๋์ด๊ฐ๋ณผ๊น? ใ ใ HMAC(Hash-based Message Authentication Code)๋ฅผ ์ฌ์ฉํ๋ฉด ํ ํฐ์ ๋ฌด๊ฒฐ์ฑ์ ๋ณด์ฅํ ์ ์์ด. ์๋ฒ๋ง ์๊ณ ์๋ ๋น๋ฐ ํค๋ก ํ ํฐ์ ์๋ช ํ๋ ๋ฐฉ์์ด์ง.
const crypto = require('crypto');
class CSRFTokenManager {
constructor(secretKey) {
this.secretKey = secretKey;
}
// CSRF ํ ํฐ ์์ฑ
generateToken(sessionId, timestamp = Date.now()) {
// ๋๋ค ๊ฐ ์์ฑ
const randomValue = crypto.randomBytes(16).toString('hex');
// ํ ํฐ ํ์ด๋ก๋: sessionId + timestamp + randomValue
const payload = `${sessionId}:${timestamp}:${randomValue}`;
// HMAC ์๋ช
์์ฑ
const signature = crypto
.createHmac('sha256', this.secretKey)
.update(payload)
.digest('hex');
// ์ต์ข
ํ ํฐ: payload + signature
const token = `${payload}:${signature}`;
return Buffer.from(token).toString('base64');
}
// CSRF ํ ํฐ ๊ฒ์ฆ
verifyToken(token, sessionId, maxAge = 3600000) {
try {
// Base64 ๋์ฝ๋ฉ
const decoded = Buffer.from(token, 'base64').toString('utf-8');
const parts = decoded.split(':');
if (parts.length !== 4) {
return { valid: false, reason: 'ํ ํฐ ํ์ ์ค๋ฅ' };
}
const [tokenSessionId, timestamp, randomValue, signature] = parts;
// ์ธ์
ID ํ์ธ
if (tokenSessionId !== sessionId) {
return { valid: false, reason: '์ธ์
๋ถ์ผ์น' };
}
// ํ์์คํฌํ ํ์ธ (๋ง๋ฃ ์ฒดํฌ)
const tokenAge = Date.now() - parseInt(timestamp);
if (tokenAge > maxAge) {
return { valid: false, reason: 'ํ ํฐ ๋ง๋ฃ๋จ โฐ' };
}
// ์๋ช
๊ฒ์ฆ
const payload = `${tokenSessionId}:${timestamp}:${randomValue}`;
const expectedSignature = crypto
.createHmac('sha256', this.secretKey)
.update(payload)
.digest('hex');
if (signature !== expectedSignature) {
return { valid: false, reason: '์๋ช
๊ฒ์ฆ ์คํจ ๐ซ' };
}
return { valid: true, reason: 'ํ ํฐ ์ ํจ โ
' };
} catch (error) {
return { valid: false, reason: 'ํ ํฐ ํ์ฑ ์ค๋ฅ' };
}
}
}
// ์ฌ์ฉ ์์
const secretKey = crypto.randomBytes(32).toString('hex');
const csrfManager = new CSRFTokenManager(secretKey);
// ํ ํฐ ์์ฑ
const sessionId = 'user-session-12345';
const token = csrfManager.generateToken(sessionId);
console.log('์์ฑ๋ ํ ํฐ:', token);
// ํ ํฐ ๊ฒ์ฆ
const verification = csrfManager.verifyToken(token, sessionId);
console.log('๊ฒ์ฆ ๊ฒฐ๊ณผ:', verification);
// ์ถ๋ ฅ: { valid: true, reason: 'ํ ํฐ ์ ํจ โ
' }
const express = require('express');
const session = require('express-session');
const app = express();
// ์ํฌ๋ฆฟ ํค๋ ํ๊ฒฝ๋ณ์์์ ๊ฐ์ ธ์ค๊ธฐ
const SECRET_KEY = process.env.CSRF_SECRET || crypto.randomBytes(32).toString('hex');
const csrfManager = new CSRFTokenManager(SECRET_KEY);
app.use(session({
secret: 'session-secret',
resave: false,
saveUninitialized: true
}));
// CSRF ํ ํฐ ์์ฑ ๋ฏธ๋ค์จ์ด
app.use((req, res, next) => {
if (req.session && req.session.id) {
req.csrfToken = () => csrfManager.generateToken(req.session.id);
res.locals.csrfToken = req.csrfToken();
}
next();
});
// CSRF ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function validateCSRF(req, res, next) {
const token = req.body._csrf || req.headers['x-csrf-token'];
if (!token) {
return res.status(403).json({ error: 'CSRF ํ ํฐ ๋๋ฝ' });
}
const result = csrfManager.verifyToken(token, req.session.id);
if (!result.valid) {
return res.status(403).json({
error: 'CSRF ๊ฒ์ฆ ์คํจ',
reason: result.reason
});
}
next();
}
// ๋ผ์ฐํธ ์์
app.get('/form', (req, res) => {
res.send(`
<form method="POST" action="/submit">
<input type="hidden" name="_csrf" value="${res.locals.csrfToken}">
<input type="text" name="data">
<button>์ ์ก</button>
</form>
`);
});
app.post('/submit', validateCSRF, (req, res) => {
res.json({ success: true, message: '์์ ํ๊ฒ ์ฒ๋ฆฌ๋จ! ๐' });
});
app.listen(3000);
HMAC ๋ฐฉ์์ ์ฅ์ ์ ํ ํฐ ์์ฒด์ ๊ฒ์ฆ ์ ๋ณด๊ฐ ํฌํจ๋์ด ์๋ค๋ ๊ฑฐ์ผ. ์๋ฒ๊ฐ ๋ณ๋๋ก ํ ํฐ์ ์ ์ฅํ์ง ์์๋ ๋๊ณ , ํ ํฐ์ด ์์กฐ๋์๋์ง ์ฆ์ ์ ์ ์์ง ใ ใ ๋ณด์์ฑ๋ ํจ์ฌ ๋๊ณ !
๐ฑ ๋ชจ๋ฐ์ผ ์ฑ์์์ CSRF ๋ฐฉ์ด
๋ชจ๋ฐ์ผ ์ฑ์ ์น๊ณผ ์ข ๋ค๋ฅด๊ฒ ์ ๊ทผํด์ผ ํด ใ ใ ๋ค์ดํฐ๋ธ ์ฑ์ ๋ธ๋ผ์ฐ์ ๊ฐ ์๋๋๊น ์ ํต์ ์ธ CSRF ๊ณต๊ฒฉ์๋ ๋ ์ทจ์ฝํ์ง๋ง, ๊ทธ๋๋ ๋ฐฉ์ด ๋ฉ์ปค๋์ฆ์ ํ์ํด!
// csrfService.js
import AsyncStorage from '@react-native-async-storage/async-storage';
class CSRFService {
constructor() {
this.tokenKey = '@csrf_token';
this.baseURL = 'https://api.example.com';
}
// ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
async getToken() {
try {
const token = await AsyncStorage.getItem(this.tokenKey);
if (token) {
return token;
}
// ํ ํฐ์ด ์์ผ๋ฉด ์๋ฒ์์ ์๋ก ๋ฐ์์ค๊ธฐ
return await this.fetchNewToken();
} catch (error) {
console.error('ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ ์คํจ ใ
ใ
', error);
return null;
}
}
// ์๋ฒ์์ ์ ํ ํฐ ๋ฐ์์ค๊ธฐ
async fetchNewToken() {
try {
const response = await fetch(`${this.baseURL}/api/csrf-token`, {
method: 'GET',
credentials: 'include'
});
const data = await response.json();
await AsyncStorage.setItem(this.tokenKey, data.token);
return data.token;
} catch (error) {
console.error('ํ ํฐ ๋ฐ๊ธ ์คํจ:', error);
return null;
}
}
// ํ ํฐ ๊ฐฑ์
async refreshToken() {
await AsyncStorage.removeItem(this.tokenKey);
return await this.fetchNewToken();
}
// API ํธ์ถ ๋ํผ
async apiCall(endpoint, options = {}) {
const token = await this.getToken();
const defaultOptions = {
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token,
...options.headers
}
};
try {
const response = await fetch(
`${this.baseURL}${endpoint}`,
{ ...defaultOptions, ...options }
);
// 403 ์๋ฌ๋ฉด ํ ํฐ ๊ฐฑ์ ํ ์ฌ์๋
if (response.status === 403) {
console.log('ํ ํฐ ๋ง๋ฃ, ๊ฐฑ์ ์ค...');
await this.refreshToken();
// ์ฌ์๋
const newToken = await this.getToken();
defaultOptions.headers['X-CSRF-Token'] = newToken;
return await fetch(
`${this.baseURL}${endpoint}`,
{ ...defaultOptions, ...options }
);
}
return response;
} catch (error) {
console.error('API ํธ์ถ ์คํจ:', error);
throw error;
}
}
}
export default new CSRFService();
import React, { useState } from 'react';
import { View, TextInput, Button, Text, Alert } from 'react-native';
import csrfService from './csrfService';
function ProtectedForm() {
const [name, setName] = useState('');
const [email, setEmail] = useState('');
const [loading, setLoading] = useState(false);
const handleSubmit = async () => {
setLoading(true);
try {
const response = await csrfService.apiCall('/api/submit', {
method: 'POST',
body: JSON.stringify({ name, email })
});
const result = await response.json();
if (response.ok) {
Alert.alert('์ฑ๊ณต', '๋ฐ์ดํฐ๊ฐ ์ ์ก๋์์ต๋๋ค! ๐');
setName('');
setEmail('');
} else {
Alert.alert('์ค๋ฅ', result.error || '์ ์ก ์คํจ ใ
ใ
');
}
} catch (error) {
Alert.alert('์ค๋ฅ', '๋คํธ์ํฌ ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค');
} finally {
setLoading(false);
}
};
return (
<View style={{ padding: 20 }}>
<TextInput
placeholder="์ด๋ฆ"
value={name}
onChangeText={setName}
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<TextInput
placeholder="์ด๋ฉ์ผ"
value={email}
onChangeText={setEmail}
keyboardType="email-address"
style={{ borderWidth: 1, padding: 10, marginBottom: 10 }}
/>
<Button
title={loading ? "์ ์ก ์ค..." : "์ ์ก"}
onPress={handleSubmit}
disabled={loading}
/>
</View>
);
}
export default ProtectedForm;
๋ชจ๋ฐ์ผ ์ฑ์์๋ ํ ํฐ์ AsyncStorage๋ SecureStore์ ์ ์ฅํ๊ณ , API ํธ์ถ ์๋ง๋ค ์๋์ผ๋ก ํฌํจ์ํค๋ ๊ฒ ์ผ๋ฐ์ ์ด์ผ. ํ ํฐ ๋ง๋ฃ ์ ์๋ ๊ฐฑ์ ๋ก์ง๋ ๊ผญ ๋ฃ์ด์ค์ผ ํ๊ณ !
๐ ํ ํฐ ๊ฐฑ์ ์ ๋ต
CSRF ํ ํฐ๋ ๋ง๋ฃ ์๊ฐ์ด ์์ด์ผ ํด. ๋๋ฌด ์ค๋ ์ ํจํ๋ฉด ๋ณด์ ์ํ์ด ์ปค์ง๊ฑฐ๋ ใ ใ ๊ทผ๋ฐ ์ฌ์ฉ์๊ฐ ํผ ์์ฑ ์ค์ ํ ํฐ์ด ๋ง๋ฃ๋๋ฉด? ์ง์ฆ๋์ง ใ ใ ๊ทธ๋์ ์ ์ ํ ๊ฐฑ์ ์ ๋ต์ด ํ์ํด!
class TokenRefreshManager {
constructor(refreshInterval = 1800000) { // ๊ธฐ๋ณธ 30๋ถ
this.refreshInterval = refreshInterval;
this.refreshTimer = null;
this.token = null;
}
// ํ ํฐ ๊ฐฑ์ ์์
startAutoRefresh() {
// ์ด๊ธฐ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
this.refreshToken();
// ์ฃผ๊ธฐ์ ๊ฐฑ์ ์ค์
this.refreshTimer = setInterval(() => {
this.refreshToken();
}, this.refreshInterval);
console.log('์๋ ํ ํฐ ๊ฐฑ์ ์์ ๐');
}
// ํ ํฐ ๊ฐฑ์ ์ค์ง
stopAutoRefresh() {
if (this.refreshTimer) {
clearInterval(this.refreshTimer);
this.refreshTimer = null;
console.log('์๋ ํ ํฐ ๊ฐฑ์ ์ค์ง โธ๏ธ');
}
}
// ํ ํฐ ๊ฐฑ์
async refreshToken() {
try {
const response = await fetch('/api/csrf-token/refresh', {
method: 'POST',
credentials: 'include'
});
if (response.ok) {
const data = await response.json();
this.token = data.token;
// ์ปค์คํ
์ด๋ฒคํธ ๋ฐ์ (๋ค๋ฅธ ์ปดํฌ๋ํธ์์ ๊ฐ์ง ๊ฐ๋ฅ)
window.dispatchEvent(new CustomEvent('csrf-token-refreshed', {
detail: { token: this.token }
}));
console.log('ํ ํฐ ๊ฐฑ์ ์๋ฃ โ
');
return this.token;
} else {
console.error('ํ ํฐ ๊ฐฑ์ ์คํจ ใ
ใ
');
return null;
}
} catch (error) {
console.error('ํ ํฐ ๊ฐฑ์ ์ค ์ค๋ฅ:', error);
return null;
}
}
// ํ์ฌ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
getToken() {
return this.token;
}
}
// ์ ์ญ ์ธ์คํด์ค ์์ฑ
const tokenManager = new TokenRefreshManager();
// ํ์ด์ง ๋ก๋ ์ ์๋ ๊ฐฑ์ ์์
window.addEventListener('load', () => {
tokenManager.startAutoRefresh();
});
// ํ์ด์ง ์ธ๋ก๋ ์ ๊ฐฑ์ ์ค์ง
window.addEventListener('beforeunload', () => {
tokenManager.stopAutoRefresh();
});
export default tokenManager;
import { useState, useEffect, useCallback } from 'react';
function useCSRFToken(refreshInterval = 1800000) {
const [token, setToken] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
// ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
const fetchToken = useCallback(async () => {
try {
setLoading(true);
const response = await fetch('/api/csrf-token');
if (!response.ok) {
throw new Error('ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ ์คํจ');
}
const data = await response.json();
setToken(data.token);
setError(null);
} catch (err) {
setError(err.message);
console.error('ํ ํฐ ๋ก๋ ์ค๋ฅ:', err);
} finally {
setLoading(false);
}
}, []);
// ํ ํฐ ๊ฐฑ์
const refreshToken = useCallback(async () => {
console.log('ํ ํฐ ๊ฐฑ์ ์ค... ๐');
await fetchToken();
}, [fetchToken]);
// ์ด๊ธฐ ๋ก๋ ๋ฐ ์๋ ๊ฐฑ์ ์ค์
useEffect(() => {
fetchToken();
// ์ฃผ๊ธฐ์ ๊ฐฑ์
const interval = setInterval(refreshToken, refreshInterval);
return () => {
clearInterval(interval);
};
}, [fetchToken, refreshToken, refreshInterval]);
return { token, loading, error, refreshToken };
}
// ์ฌ์ฉ ์์
function MyComponent() {
const { token, loading, error, refreshToken } = useCSRFToken();
if (loading) return <div>ํ ํฐ ๋ก๋ฉ ์ค... โณ</div>;
if (error) return <div>์ค๋ฅ: {error} ๐ข</div>;
const handleSubmit = async (data) => {
try {
const response = await fetch('/api/submit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify(data)
});
if (response.status === 403) {
// ํ ํฐ ๋ง๋ฃ ์ ๊ฐฑ์ ํ ์ฌ์๋
await refreshToken();
// ์ฌ์๋ ๋ก์ง...
}
} catch (error) {
console.error('์ ์ก ์คํจ:', error);
}
};
return (
<div>
<p>ํ์ฌ ํ ํฐ: {token.substring(0, 20)}...</p>
<button onClick={() => handleSubmit({ test: 'data' })}>
์ ์ก
</button>
</div>
);
}
2. ๋ฐฑ๊ทธ๋ผ์ด๋ ๊ฐฑ์ : ์ฌ์ฉ์ ๊ฒฝํ์ ๋ฐฉํดํ์ง ์๋๋ก ์กฐ์ฉํ ๊ฐฑ์
3. ์คํจ ์ฒ๋ฆฌ: ๊ฐฑ์ ์คํจ ์ ์ฌ์๋ ๋ก์ง ๊ตฌํ
4. ์ฌ์ฉ์ ํ๋ ๊ฐ์ง: ์ฌ์ฉ์๊ฐ ํ๋ ์ค์ผ ๋๋ง ๊ฐฑ์ (๋ฆฌ์์ค ์ ์ฝ)
5. ๋ง๋ฃ ์๋ฐ ๊ฒฝ๊ณ : ํ ํฐ์ด ๊ณง ๋ง๋ฃ๋ ๋ ์ฌ์ฉ์์๊ฒ ์๋ฆผ
๐ ๏ธ ์ค์ ๋๋ฒ๊น ํ
CSRF ํ ํฐ ๊ตฌํํ๋ค ๋ณด๋ฉด ๋ฌธ์ ๊ฐ ์๊ธธ ๋๊ฐ ๋ง์ ใ ใ ใ ๋ด๊ฐ ๊ฒช์๋ ์ฝ์ง๋ค์ ๊ณต์ ํด๋ณผ๊ฒ!
โ ์ฟ ํค ์ค์ ํ์ธ! sameSite, secure ์์ฑ ์ฒดํฌ
โ ๋๋ฉ์ธ์ด ๋ค๋ฅด๋ฉด CORS ์ค์ ํ์
โ ์ธ์ ์ด ์ ๋๋ก ์ ์ง๋๋์ง ํ์ธ
2. AJAX ์์ฒญ์์๋ง ์คํจํ๋ค?
โ credentials: 'include' ๋น ๋จ๋ ธ์ ๊ฐ๋ฅ์ฑ ๋์
โ ํค๋์ ํ ํฐ ์ ๋๋ก ๋ฃ์๋์ง ํ์ธ
โ preflight ์์ฒญ(OPTIONS) ์ฒ๋ฆฌ ํ์ธ
3. ๋ก์ปฌ์์๋ ๋๋๋ฐ ๋ฐฐํฌํ๋ฉด ์ ๋๋ค?
โ HTTPS ์ค์ ํ์ธ (secure ์ฟ ํค๋ HTTPS ํ์)
โ ํ๋ก์ ์ค์ ๋ฌธ์ ์ผ ์ ์์
โ ํ๊ฒฝ๋ณ์ ์ ๋๋ก ์ค์ ๋๋์ง ํ์ธ
// CSRF ํ ํฐ ๋๋ฒ๊น
์ ํธ๋ฆฌํฐ
class CSRFDebugger {
static logTokenInfo(token, sessionId) {
console.group('๐ CSRF ํ ํฐ ์ ๋ณด');
console.log('ํ ํฐ:', token);
console.log('ํ ํฐ ๊ธธ์ด:', token?.length);
console.log('์ธ์
ID:', sessionId);
console.log('์ฟ ํค:', document.cookie);
console.groupEnd();
}
static validateTokenFormat(token) {
if (!token) {
console.error('โ ํ ํฐ์ด ์์!');
return false;
}
if (typeof token !== 'string') {
console.error('โ ํ ํฐ์ด ๋ฌธ์์ด์ด ์๋!');
return false;
}
if (token.length < 32) {
console.warn('โ ๏ธ ํ ํฐ์ด ๋๋ฌด ์งง์ (๋ณด์ ์ทจ์ฝ)');
return false;
}
console.log('โ
ํ ํฐ ํ์ ์ ํจ');
return true;
}
static async testCSRFProtection(endpoint, token) {
console.group('๐งช CSRF ๋ณดํธ ํ
์คํธ');
// 1. ํ ํฐ ์์ด ์์ฒญ
console.log('ํ
์คํธ 1: ํ ํฐ ์์ด ์์ฒญ');
try {
const res1 = await fetch(endpoint, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ test: 'data' })
});
console.log('๊ฒฐ๊ณผ:', res1.status, res1.status === 403 ? 'โ
์ฐจ๋จ๋จ' : 'โ ํต๊ณผ๋จ');
} catch (e) {
console.error('์ค๋ฅ:', e);
}
// 2. ์๋ชป๋ ํ ํฐ์ผ๋ก ์์ฒญ
console.log('ํ
์คํธ 2: ์๋ชป๋ ํ ํฐ์ผ๋ก ์์ฒญ');
try {
const res2 = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': 'invalid-token-12345'
},
body: JSON.stringify({ test: 'data' })
});
console.log('๊ฒฐ๊ณผ:', res2.status, res2.status === 403 ? 'โ
์ฐจ๋จ๋จ' : 'โ ํต๊ณผ๋จ');
} catch (e) {
console.error('์ค๋ฅ:', e);
}
// 3. ์ฌ๋ฐ๋ฅธ ํ ํฐ์ผ๋ก ์์ฒญ
console.log('ํ
์คํธ 3: ์ฌ๋ฐ๋ฅธ ํ ํฐ์ผ๋ก ์์ฒญ');
try {
const res3 = await fetch(endpoint, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ test: 'data' })
});
console.log('๊ฒฐ๊ณผ:', res3.status, res3.status === 200 ? 'โ
ํต๊ณผ๋จ' : 'โ ์ฐจ๋จ๋จ');
} catch (e) {
console.error('์ค๋ฅ:', e);
}
console.groupEnd();
}
static monitorCSRFRequests() {
// Fetch API ๋ํ
const originalFetch = window.fetch;
window.fetch = async (...args) => {
const [url, options] = args;
if (options?.method && options.method !== 'GET') {
const token = options.headers?.['X-CSRF-Token'] ||
options.headers?.['x-csrf-token'];
console.log('๐ CSRF ๋ณดํธ ์์ฒญ:', {
url,
method: options.method,
hasToken: !!token,
token: token?.substring(0, 20) + '...'
});
}
return originalFetch(...args);
};
console.log('โ
CSRF ์์ฒญ ๋ชจ๋ํฐ๋ง ํ์ฑํ');
}
}
// ๊ฐ๋ฐ ํ๊ฒฝ์์๋ง ์ฌ์ฉ
if (process.env.NODE_ENV === 'development') {
window.CSRFDebugger = CSRFDebugger;
// ์๋ ๋ชจ๋ํฐ๋ง ์์
CSRFDebugger.monitorCSRFRequests();
}
๋๋ฒ๊น ๋๊ตฌ๋ฅผ ๋ง๋ค์ด๋๋ฉด ์ง์ง ํธํด ใ ใ ใ ๋ฌธ์ ์๊ฒผ์ ๋ ๋ฐ๋ก ์์ธ ํ์ ํ ์ ์๊ฑฐ๋ . ํนํ ํ๋ก๋์ ๋ฐฐํฌ ์ ์ ๊ผญ ํ ์คํธํด๋ด์ผ ํด!
๐ ๋ค์ํ ํ๋ ์์ํฌ๋ณ ๊ตฌํ
์ง๊ธ๊น์ง ์ฃผ๋ก Node.js๋ Python์ ๋ดค๋๋ฐ, ๋ค๋ฅธ ์ธ์ด๋ค๋ ๊ฐ๋จํ ์ดํด๋ณผ๊ฒ ใ ใ
use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
use actix_session::{Session, SessionMiddleware, storage::CookieSessionStore};
use actix_web::cookie::Key;
use rand::{thread_rng, Rng};
use rand::distributions::Alphanumeric;
// CSRF ํ ํฐ ์์ฑ
fn generate_csrf_token() -> String {
thread_rng()
.sample_iter(&Alphanumeric)
.take(32)
.map(char::from)
.collect()
}
// CSRF ํ ํฐ ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
async fn verify_csrf(req: HttpRequest, session: Session) -> Result<(), HttpResponse> {
let session_token = session.get::<String>("csrf_token")?
.ok_or_else(|| HttpResponse::Forbidden().body("No CSRF token in session"))?;
let request_token = req
.headers()
.get("X-CSRF-Token")
.and_then(|h| h.to_str().ok())
.ok_or_else(|| HttpResponse::Forbidden().body("No CSRF token in request"))?;
if session_token != request_token {
return Err(HttpResponse::Forbidden().body("CSRF token mismatch"));
}
Ok(())
}
// ๋ผ์ฐํธ ํธ๋ค๋ฌ
async fn get_form(session: Session) -> HttpResponse {
let token = generate_csrf_token();
session.insert("csrf_token", token.clone()).unwrap();
HttpResponse::Ok().body(format!(
r#"<form method="POST" action="/submit">
<input type="hidden" name="csrf_token" value="{}">
<button>Submit</button>
</form>"#,
token
))
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
let secret_key = Key::generate();
HttpServer::new(move || {
App::new()
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone()
))
.route("/form", web::get().to(get_form))
})
.bind(("127.0.0.1", 8080))?
.run()
.await
}
# Rails๋ ๊ธฐ๋ณธ์ ์ผ๋ก CSRF ๋ณดํธ๊ฐ ํ์ฑํ๋์ด ์์
# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
# CSRF ๋ณดํธ ํ์ฑํ (๊ธฐ๋ณธ๊ฐ)
protect_from_forgery with: :exception
# ํน์ ์ก์
์์ CSRF ๋ณดํธ ๊ฑด๋๋ฐ๊ธฐ
skip_before_action :verify_authenticity_token, only: [:webhook]
# API ์ปจํธ๋กค๋ฌ์์๋ null_session ์ฌ์ฉ
# protect_from_forgery with: :null_session
end
# ๋ทฐ์์ CSRF ํ ํฐ ์ฌ์ฉ
# app/views/posts/new.html.erb
<%= form_with model: @post do |form| %>
<!-- csrf_meta_tags ํฌํผ๊ฐ ์๋์ผ๋ก ํ ํฐ ์ถ๊ฐ -->
<%= form.text_field :title %>
<%= form.submit %>
<% end %>
# JavaScript์์ CSRF ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
# app/javascript/csrf.js
const token = document.querySelector('meta[name="csrf-token"]').content;
fetch('/api/data', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': token
},
body: JSON.stringify({ data: 'test' })
});
<?php
// Laravel๋ ๊ธฐ๋ณธ์ ์ผ๋ก CSRF ๋ณดํธ ํ์ฑํ
// app/Http/Middleware/VerifyCsrfToken.php
namespace App\Http\Middleware;
use Illuminate\Foundation\Http\Middleware\VerifyCsrfToken as Middleware;
class VerifyCsrfToken extends Middleware
{
// CSRF ๋ณดํธ ์ ์ธํ URI
protected $except = [
'api/*',
'webhook/*'
];
}
// ๋ธ๋ ์ด๋ ํ
ํ๋ฆฟ์์ ์ฌ์ฉ
// resources/views/form.blade.php
<form method="POST" action="/submit">
@csrf <!-- ์๋์ผ๋ก CSRF ํ ํฐ ํ๋ ์์ฑ -->
<input type="text" name="data">
<button type="submit">์ ์ก</button>
</form>
// JavaScript์์ ์ฌ์ฉ
// resources/js/app.js
axios.defaults.headers.common['X-CSRF-TOKEN'] =
document.querySelector('meta[name="csrf-token"]').content;
// API ๋ผ์ฐํธ์์ ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
Route::get('/csrf-token', function () {
return response()->json(['token' => csrf_token()]);
});
?>
๋ณด๋ค์ํผ ๋๋ถ๋ถ์ ํ๋์ ์ธ ํ๋ ์์ํฌ๋ค์ CSRF ๋ณดํธ๋ฅผ ๊ธฐ๋ณธ์ผ๋ก ์ ๊ณตํด ใ ใ ๊ฐ๋ฐ์๊ฐ ์ง์ ๊ตฌํํ ํ์ ์์ด ์ค์ ๋ง ์ ํด์ฃผ๋ฉด ๋ผ. ๊ทผ๋ฐ ์๋ฆฌ๋ฅผ ์ดํดํ๋ ๊ฑด ์ฌ์ ํ ์ค์ํด!
๐ ๋ณด์ ๊ฐํ ํ
๊ธฐ๋ณธ์ ์ธ CSRF ํ ํฐ๋ง์ผ๋ก๋ ๋ถ์กฑํ ๋๊ฐ ์์ด. ์ถ๊ฐ์ ์ธ ๋ณด์ ๋ ์ด์ด๋ฅผ ๋ํ๋ฉด ๋ ์์ ํด์ง์ง ใ ใ
| ๋ณด์ ๊ธฐ๋ฒ | ์ค๋ช | ๊ตฌํ ๋์ด๋ |
|---|---|---|
| SameSite ์ฟ ํค | ์ฟ ํค์ SameSite=Strict ๋๋ Lax ์ค์ ์ผ๋ก CSRF ์ถ๊ฐ ๋ฐฉ์ด | โญ ์ฌ์ |
| Origin ๊ฒ์ฆ | ์์ฒญ์ Origin/Referer ํค๋๋ฅผ ๊ฒ์ฆํ์ฌ ์ถ์ฒ ํ์ธ | โญโญ ๋ณดํต |
| ์ปค์คํ ํค๋ | X-Requested-With ๊ฐ์ ์ปค์คํ ํค๋ ์๊ตฌ | โญ ์ฌ์ |
| ํ ํฐ ์ํธํ | ํ ํฐ์ AES ๋ฑ์ผ๋ก ์ํธํํ์ฌ ์ ์ฅ | โญโญโญ ์ด๋ ค์ |
| Rate Limiting | ๋์ผ IP์์ ๊ณผ๋ํ ์์ฒญ ์ฐจ๋จ | โญโญ ๋ณดํต |
| CAPTCHA | ์ค์ํ ์์ ์ CAPTCHA ์ถ๊ฐ | โญโญ ๋ณดํต |
const express = require('express');
const rateLimit = require('express-rate-limit');
const app = express();
// 1. Rate Limiting
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15๋ถ
max: 100, // ์ต๋ 100๊ฐ ์์ฒญ
message: '๋๋ฌด ๋ง์ ์์ฒญ! ๋์ค์ ๋ค์ ์๋ํ์ธ์ ใ
ใ
'
});
app.use('/api/', limiter);
// 2. Origin ๊ฒ์ฆ ๋ฏธ๋ค์จ์ด
function verifyOrigin(req, res, next) {
const allowedOrigins = [
'https://example.com',
'https://www.example.com'
];
const origin = req.headers.origin || req.headers.referer;
if (!origin || !allowedOrigins.some(allowed => origin.startsWith(allowed))) {
return res.status(403).json({
error: 'ํ์ฉ๋์ง ์์ ์ถ์ฒ์
๋๋ค ๐ซ'
});
}
next();
}
// 3. ์ปค์คํ
ํค๋ ๊ฒ์ฆ
function requireCustomHeader(req, res, next) {
if (!req.headers['x-requested-with']) {
return res.status(403).json({
error: 'ํ์ ํค๋๊ฐ ๋๋ฝ๋์์ต๋๋ค'
});
}
next();
}
// 4. ์ข
ํฉ ๋ณด์ ๋ฏธ๋ค์จ์ด
function comprehensiveSecurity(req, res, next) {
// CSRF ํ ํฐ ๊ฒ์ฆ
const csrfToken = req.headers['x-csrf-token'];
if (!csrfToken || csrfToken !== req.session.csrfToken) {
return res.status(403).json({ error: 'CSRF ๊ฒ์ฆ ์คํจ' });
}
// Origin ๊ฒ์ฆ
verifyOrigin(req, res, (err) => {
if (err) return;
// ์ปค์คํ
ํค๋ ๊ฒ์ฆ
requireCustomHeader(req, res, (err) => {
if (err) return;
// ๋ชจ๋ ๊ฒ์ฆ ํต๊ณผ
next();
});
});
}
// ๋ณดํธ๋ ๋ผ์ฐํธ
app.post('/api/critical-action',
limiter,
comprehensiveSecurity,
(req, res) => {
res.json({ success: true, message: '์์ ํ๊ฒ ์ฒ๋ฆฌ๋จ! ๐' });
}
);
๐ ์ฑ๋ฅ ์ต์ ํ
๋ณด์๋ ์ค์ํ์ง๋ง ์ฑ๋ฅ๋ ๋ฌด์ํ ์ ์์ง ใ ใ CSRF ํ ํฐ ๊ฒ์ฆ์ด ๋ณ๋ชฉ์ด ๋๋ฉด ์ ๋๋๊น!
const NodeCache = require('node-cache');
class OptimizedCSRFManager {
constructor() {
// ํ ํฐ ์บ์ (TTL: 1์๊ฐ)
this.tokenCache = new NodeCache({
stdTTL: 3600,
checkperiod: 120
});
// ๊ฒ์ฆ ๊ฒฐ๊ณผ ์บ์ (TTL: 5๋ถ)
this.validationCache = new NodeCache({
stdTTL: 300,
checkperiod: 60
});
}
// ํ ํฐ ์์ฑ (์บ์ ํ์ฉ)
generateToken(sessionId) {
// ์บ์์์ ๊ธฐ์กด ํ ํฐ ํ์ธ
let token = this.tokenCache.get(sessionId);
if (!token) {
// ์ ํ ํฐ ์์ฑ
token = crypto.randomBytes(32).toString('hex');
this.tokenCache.set(sessionId, token);
console.log('์ ํ ํฐ ์์ฑ ๋ฐ ์บ์ฑ โจ');
} else {
console.log('์บ์๋ ํ ํฐ ์ฌ์ฉ โก');
}
return token;
}
// ํ ํฐ ๊ฒ์ฆ (์บ์ ํ์ฉ)
verifyToken(sessionId, token) {
// ๊ฒ์ฆ ๊ฒฐ๊ณผ ์บ์ ํค
const cacheKey = `${sessionId}:${token}`;
// ์บ์๋ ๊ฒ์ฆ ๊ฒฐ๊ณผ ํ์ธ
let isValid = this.validationCache.get(cacheKey);
if (isValid !== undefined) {
console.log('์บ์๋ ๊ฒ์ฆ ๊ฒฐ๊ณผ ์ฌ์ฉ โก');
return isValid;
}
// ์ค์ ๊ฒ์ฆ ์ํ
const storedToken = this.tokenCache.get(sessionId);
isValid = storedToken === token;
// ๊ฒ์ฆ ๊ฒฐ๊ณผ ์บ์ฑ
this.validationCache.set(cacheKey, isValid);
console.log('๊ฒ์ฆ ์ํ ๋ฐ ๊ฒฐ๊ณผ ์บ์ฑ ๐พ');
return isValid;
}
// ํต๊ณ ์ ๋ณด
getStats() {
return {
tokenCacheSize: this.tokenCache.keys().length,
validationCacheSize: this.validationCache.keys().length,
tokenCacheHits: this.tokenCache.getStats().hits,
tokenCacheMisses: this.tokenCache.getStats().misses
};
}
}
const csrfManager = new OptimizedCSRFManager();
// ์ฃผ๊ธฐ์ ์ผ๋ก ํต๊ณ ๋ก๊น
setInterval(() => {
console.log('๐ CSRF ์บ์ ํต๊ณ:', csrfManager.getStats());
}, 60000); // 1๋ถ๋ง๋ค
// ๋น๋๊ธฐ ํ ํฐ ๊ฒ์ฆ์ผ๋ก ์ฑ๋ฅ ํฅ์
async function asyncCSRFVerification(req, res, next) {
const token = req.headers['x-csrf-token'];
const sessionId = req.session.id;
// Promise๋ก ๋น๋๊ธฐ ๊ฒ์ฆ
const verificationPromise = new Promise((resolve, reject) => {
setImmediate(() => {
try {
const isValid = csrfManager.verifyToken(sessionId, token);
resolve(isValid);
} catch (error) {
reject(error);
}
});
});
try {
const isValid = await verificationPromise;
if (!isValid) {
return res.status(403).json({ error: 'CSRF ๊ฒ์ฆ ์คํจ' });
}
next();
} catch (error) {
console.error('๊ฒ์ฆ ์ค๋ฅ:', error);
res.status(500).json({ error: '์๋ฒ ์ค๋ฅ' });
}
}
// ๋ฐฐ์น ๊ฒ์ฆ (์ฌ๋ฌ ์์ฒญ ๋์ ์ฒ๋ฆฌ)
async function batchVerifyCSRF(requests) {
const verifications = requests.map(req => ({
sessionId: req.session.id,
token: req.headers['x-csrf-token']
}));
// ๋ณ๋ ฌ ๊ฒ์ฆ
const results = await Promise.all(
verifications.map(v =>
csrfManager.verifyToken(v.sessionId, v.token)
)
);
return results;
}
์บ์ฑ์ ์ ํ์ฉํ๋ฉด ์ฑ๋ฅ์ด ์์ฒญ ์ข์์ ธ ใ ใ ํนํ ํธ๋ํฝ์ด ๋ง์ ์๋น์ค์์๋ ํ์์ผ. ๊ทผ๋ฐ ์บ์ ๋ฌดํจํ ์ ๋ต๋ ์ ์ธ์์ผ ํด!
๐งช ํ ์คํธ ์ฝ๋ ์์ฑ
๋ณด์ ๊ธฐ๋ฅ์ ํ ์คํธ๊ฐ ์ง์ง ์ค์ํด! CSRF ๋ฐฉ์ด๊ฐ ์ ๋๋ก ์๋ํ๋์ง ์๋ํ๋ ํ ์คํธ๋ก ๊ฒ์ฆํด์ผ ํด.
// csrf.test.js
const CSRFTokenManager = require('./csrfTokenManager');
const crypto = require('crypto');
describe('CSRF Token Manager', () => {
let manager;
beforeEach(() => {
const secretKey = crypto.randomBytes(32).toString('hex');
manager = new CSRFTokenManager(secretKey);
});
test('ํ ํฐ์ด ์ ์์ ์ผ๋ก ์์ฑ๋์ด์ผ ํจ', () => {
const sessionId = 'test-session-123';
const token = manager.generateToken(sessionId);
expect(token).toBeDefined();
expect(typeof token).toBe('string');
expect(token.length).toBeGreaterThan(0);
});
test('์ ํจํ ํ ํฐ์ ๊ฒ์ฆ์ ํต๊ณผํด์ผ ํจ', () => {
const sessionId = 'test-session-123';
const token = manager.generateToken(sessionId);
const result = manager.verifyToken(token, sessionId);
expect(result.valid).toBe(true);
});
test('์๋ชป๋ ํ ํฐ์ ๊ฒ์ฆ์ ์คํจํด์ผ ํจ', () => {
const sessionId = 'test-session-123';
manager.generateToken(sessionId);
const fakeToken = 'fake-token-12345';
const result = manager.verifyToken(fakeToken, sessionId);
expect(result.valid).toBe(false);
});
test('๋ค๋ฅธ ์ธ์
์ ํ ํฐ์ ๊ฒ์ฆ์ ์คํจํด์ผ ํจ', () => {
const sessionId1 = 'session-1';
const sessionId2 = 'session-2';
const token = manager.generateToken(sessionId1);
const result = manager.verifyToken(token, sessionId2);
expect(result.valid).toBe(false);
});
test('๋ง๋ฃ๋ ํ ํฐ์ ๊ฒ์ฆ์ ์คํจํด์ผ ํจ', () => {
const sessionId = 'test-session-123';
const token = manager.generateToken(sessionId);
// ์๊ฐ์ ๋ฏธ๋๋ก ์ด๋ (1์๊ฐ ํ)
jest.useFakeTimers();
jest.advanceTimersByTime(3600000 + 1000);
const result = manager.verifyToken(token, sessionId, 3600000);
expect(result.valid).toBe(false);
expect(result.reason).toContain('๋ง๋ฃ');
jest.useRealTimers();
});
test('๋์ผํ ์ธ์
์์ ์ฌ๋ฌ ํ ํฐ ์์ฑ ๊ฐ๋ฅ', () => {
const sessionId = 'test-session-123';
const token1 = manager.generateToken(sessionId);
const token2 = manager.generateToken(sessionId);
expect(token1).not.toBe(token2);
expect(manager.verifyToken(token1, sessionId).valid).toBe(true);
expect(manager.verifyToken(token2, sessionId).valid).toBe(true);
});
});
// integration.test.js
const request = require('supertest');
const app = require('./app');
describe('CSRF Protection Integration Tests', () => {
let csrfToken;
let cookies;
// ํ ํฐ ๊ฐ์ ธ์ค๊ธฐ
beforeEach(async () => {
const response = await request(app)
.get('/api/csrf-token')
.expect(200);
csrfToken = response.body.token;
cookies = response.headers['set-cookie'];
});
test('GET ์์ฒญ์ CSRF ํ ํฐ ์์ด๋ ์ฑ๊ณตํด์ผ ํจ', async () => {
await request(app)
.get('/api/data')
.expect(200);
});
test('POST ์์ฒญ์ ํ ํฐ ์์ด ์คํจํด์ผ ํจ', async () => {
await request(app)
.post('/api/submit')
.send({ data: 'test' })
.expect(403);
});
test('POST ์์ฒญ์ ์ ํจํ ํ ํฐ๊ณผ ํจ๊ป ์ฑ๊ณตํด์ผ ํจ', async () => {
await request(app)
.post('/api/submit')
.set('Cookie', cookies)
.set('X-CSRF-Token', csrfToken)
.send({ data: 'test' })
.expect(200);
});
test('์๋ชป๋ ํ ํฐ์ผ๋ก POST ์์ฒญ ์ ์คํจํด์ผ ํจ', async () => {
await request(app)
.post('/api/submit')
.set('Cookie', cookies)
.set('X-CSRF-Token', 'wrong-token')
.send({ data: 'test' })
.expect(403);
});
test('ํ ํฐ ๊ฐฑ์ ์ด ์ ์ ์๋ํด์ผ ํจ', async () => {
// ์ฒซ ๋ฒ์งธ ํ ํฐ์ผ๋ก ์์ฒญ
await request(app)
.post('/api/submit')
.set('Cookie', cookies)
.set('X-CSRF-Token', csrfToken)
.send({ data: 'test1' })
.expect(200);
// ์ ํ ํฐ ๋ฐ๊ธฐ
const newResponse = await request(app)
.get('/api/csrf-token')
.set('Cookie', cookies)
.expect(200);
const newToken = newResponse.body.token;
// ์ ํ ํฐ์ผ๋ก ์์ฒญ
await request(app)
.post('/api/submit')
.set('Cookie', cookies)
.set('X-CSRF-Token', newToken)
.send({ data: 'test2' })
.expect(200);
});
});
ํ ์คํธ ์ฝ๋ ์์ฑํ๋ฉด ๋ฆฌํฉํ ๋งํ ๋๋ ์์ฌ์ด ๋ผ ใ ใ ๋ญ๊ฐ ๋ฐ๊ฟจ๋๋ฐ ๋ณด์์ด ๋ซ๋ ธ๋ค? ๊ทธ๋ฐ ์ผ ์๊ฒ ํด์ฃผ๊ฑฐ๋ !
๐ฏ ์ค์ ์ฒดํฌ๋ฆฌ์คํธ
๋ง์ง๋ง์ผ๋ก CSRF ๋ฐฉ์ด ๊ตฌํํ ๋ ๊ผญ ํ์ธํด์ผ ํ ์ฒดํฌ๋ฆฌ์คํธ ์ ๋ฆฌํด์ค๊ฒ!
โ ์ํธํ์ ์ผ๋ก ์์ ํ ๋์ ์์ฑ๊ธฐ ์ฌ์ฉ (crypto.randomBytes ๋ฑ)
โ ์ถฉ๋ถํ ๊ธธ์ด์ ํ ํฐ (์ต์ 128๋นํธ)
โ ์์ธก ๋ถ๊ฐ๋ฅํ ํ ํฐ ์์ฑ
โ ์ธ์ /์ฌ์ฉ์๋ณ๋ก ๊ณ ์ ํ ํ ํฐ
๐ก๏ธ ํ ํฐ ์ ์ฅ
โ ์๋ฒ ์ธก์ ์์ ํ๊ฒ ์ ์ฅ (์ธ์ , DB, ์บ์)
โ ํด๋ผ์ด์ธํธ์๋ ์ฟ ํค ๋๋ ํ๋ ํ๋๋ก ์ ๋ฌ
โ httpOnly, secure, sameSite ์ฟ ํค ์์ฑ ์ค์
โ ์ ์ ํ ๋ง๋ฃ ์๊ฐ ์ค์
โ๏ธ ํ ํฐ ๊ฒ์ฆ
โ ๋ชจ๋ ์ํ ๋ณ๊ฒฝ ์์ฒญ(POST, PUT, DELETE)์ ๊ฒ์ฆ ์ ์ฉ
โ GET ์์ฒญ์ ๊ฒ์ฆํ์ง ์์
โ ํ ํฐ ์ผ์น ์ฌ๋ถ ํ์ธ
โ ํ ํฐ ๋ง๋ฃ ์ฌ๋ถ ํ์ธ
โ ๊ฒ์ฆ ์คํจ ์ ์ ์ ํ ์๋ฌ ์๋ต (403 Forbidden)
๐ CORS ๋ฐ ํค๋
โ CORS ์ค์ ํ์ธ
โ Origin/Referer ํค๋ ๊ฒ์ฆ
โ ์ปค์คํ ํค๋ ํ์ฉ (X-Requested-With ๋ฑ)
โ Content-Type ๊ฒ์ฆ
๐ ํ ํฐ ๊ด๋ฆฌ
โ ํ ํฐ ๊ฐฑ์ ๋ฉ์ปค๋์ฆ ๊ตฌํ
โ ๋ก๊ทธ์์ ์ ํ ํฐ ๋ฌดํจํ
โ ์ธ์ ๋ง๋ฃ ์ ํ ํฐ ์ญ์
โ ํ ํฐ ์ฌ์ฌ์ฉ ๋ฐฉ์ง (ํ์์)
๐งช ํ ์คํธ
โ ๋จ์ ํ ์คํธ ์์ฑ
โ ํตํฉ ํ ์คํธ ์์ฑ
โ ๋ณด์ ํ ์คํธ ์ํ
โ ๋ค์ํ ๊ณต๊ฒฉ ์๋๋ฆฌ์ค ํ ์คํธ
๐ ๋ชจ๋ํฐ๋ง
โ CSRF ๊ณต๊ฒฉ ์๋ ๋ก๊น
โ ๊ฒ์ฆ ์คํจ ๋ชจ๋ํฐ๋ง
โ ์ฑ๋ฅ ๋ฉํธ๋ฆญ ์์ง
โ ์๋ฆผ ์ค์
๐ ๋ฌธ์ํ
โ API ๋ฌธ์์ CSRF ํ ํฐ ์๊ตฌ์ฌํญ ๋ช ์
โ ํด๋ผ์ด์ธํธ ๊ฐ๋ฐ์๋ฅผ ์ํ ๊ฐ์ด๋ ์์ฑ
โ ์๋ฌ ์ฝ๋ ๋ฐ ๋ฉ์์ง ๋ฌธ์ํ
โ ๋ณด์ ์ ์ฑ ๋ฌธ์ํ
๐ฌ ๋ง๋ฌด๋ฆฌํ๋ฉฐ
์ ์ฌ๊ธฐ๊น์ง ์ฝ์์ผ๋ฉด ์ง์ง ๋๋จํ ๊ฑฐ์ผ ใ
ใ
ใ
CSRF ๋ฐฉ์ด์ ๋ํด ๊ฝค ๋ง์ด ์๊ฒ ๋์ ๊ฑฐ์ผ!
ํต์ฌ๋ง ๋ค์ ์ ๋ฆฌํ๋ฉด:
1๏ธโฃ CSRF๋ ์ฌ์ฉ์ ๋ชจ๋ฅด๊ฒ ์
์์ ์ธ ์์ฒญ์ ๋ณด๋ด๋ ๊ณต๊ฒฉ
2๏ธโฃ ํ ํฐ ๊ธฐ๋ฐ ๋ฐฉ์ด๊ฐ ๊ฐ์ฅ ํจ๊ณผ์
3๏ธโฃ ์ํธํ์ ์ผ๋ก ์์ ํ ๋๋ค ํ ํฐ ์์ฑ ํ์
4๏ธโฃ ๋ชจ๋ ์ํ ๋ณ๊ฒฝ ์์ฒญ์ ํ ํฐ ๊ฒ์ฆ ์ ์ฉ
5๏ธโฃ ํ๋ ์์ํฌ ๋ด์ฅ ๊ธฐ๋ฅ ์ ๊ทน ํ์ฉ
6๏ธโฃ ๋ค์ธต ๋ฐฉ์ด ์ ๋ต์ผ๋ก ๋ณด์ ๊ฐํ
7๏ธโฃ ํ
์คํธ์ ๋ชจ๋ํฐ๋ง ํ์!
๋ณด์์ ํ ๋ฒ ๊ตฌํํ๊ณ ๋์ด ์๋์ผ. ์ง์์ ์ผ๋ก ์
๋ฐ์ดํธํ๊ณ ๋ชจ๋ํฐ๋งํด์ผ ํด. ์๋ก์ด ๊ณต๊ฒฉ ๊ธฐ๋ฒ๋ ๊ณ์ ๋์ค๋๊น ๋ง์ด์ง ใ
ใ
ํน์ ๊ตฌํํ๋ค๊ฐ ๋งํ๊ฑฐ๋ ๋์์ด ํ์ํ๋ฉด ์ฌ๋ฅ๋ท์์ ์ ๋ฌธ๊ฐ๋ค์ ๋์์ ๋ฐ๋ ๊ฒ๋ ์ข์ ๋ฐฉ๋ฒ์ด์ผ. ํผ์ ๊ณ ๋ฏผํ์ง ๋ง๊ณ ํจ๊ป ํด๊ฒฐํด๋๊ฐ์๊ตฌ!
๊ทธ๋ผ ๋ชจ๋ ์์ ํ ์ฝ๋ฉ ํ์๊ธธ ๋ฐ๋์! ํ์ดํ
๐โจ
๐ ์ด ๊ธ์ด ๋์์ด ๋์๋์?
CSRF ๋ฐฉ์ด๋ถํฐ ๋ค์ํ ๋ณด์ ๊ธฐ์ ๊น์ง,
๋ ๋ง์ ๊ฐ๋ฐ ์ง์์ ์ฌ๋ฅ๋ท ์ง์์ธ์ ์ฒ์์ ๋ง๋๋ณด์ธ์!
ํ๋ก๊ทธ๋๋ฐ ๊ณ ์๋ค์ ๋
ธํ์ฐ๊ฐ ๊ฐ๋ํฉ๋๋ค โจ
๊ด๋ จ ํค์๋
๋๊ธ ์์ฑ
์ด ๊ธ์ ๋ํ ์ฌ๋ฌ๋ถ์ ์๊ฐ์ ๋ค๋ ค์ฃผ์ธ์
๋ก๊ทธ์ธ์ด ํ์ํฉ๋๋ค
๋๊ธ์ ์์ฑํ๋ ค๋ฉด ๋จผ์ ๋ก๊ทธ์ธํด์ฃผ์ธ์.
๋๊ธ 0
์ง์์ธ์ ์ฒ - ์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
์ง์ ์ฌ์ฐ๊ถ ๋ณดํธ ๊ณ ์ง
- ์ ์๊ถ ๋ฐ ์์ ๊ถ: ๋ณธ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ๋ ์ AI ๊ธฐ์ ๋ก ์์ฑ๋์์ผ๋ฉฐ, ๋ํ๋ฏผ๊ตญ ์ ์๊ถ๋ฒ ๋ฐ ๊ตญ์ ์ ์๊ถ ํ์ฝ์ ์ํด ๋ณดํธ๋ฉ๋๋ค.
- AI ์์ฑ ์ปจํ ์ธ ์ ๋ฒ์ ์ง์: ๋ณธ AI ์์ฑ ์ปจํ ์ธ ๋ ์ฌ๋ฅ๋ท์ ์ง์ ์ฐฝ์๋ฌผ๋ก ์ธ์ ๋๋ฉฐ, ๊ด๋ จ ๋ฒ๊ท์ ๋ฐ๋ผ ์ ์๊ถ ๋ณดํธ๋ฅผ ๋ฐ์ต๋๋ค.
- ์ฌ์ฉ ์ ํ: ์ฌ๋ฅ๋ท์ ๋ช ์์ ์๋ฉด ๋์ ์์ด ๋ณธ ์ปจํ ์ธ ๋ฅผ ๋ณต์ , ์์ , ๋ฐฐํฌ, ๋๋ ์์ ์ ์ผ๋ก ํ์ฉํ๋ ํ์๋ ์๊ฒฉํ ๊ธ์ง๋ฉ๋๋ค.
- ๋ฐ์ดํฐ ์์ง ๊ธ์ง: ๋ณธ ์ปจํ ์ธ ์ ๋ํ ๋ฌด๋จ ์คํฌ๋ํ, ํฌ๋กค๋ง, ๋ฐ ์๋ํ๋ ๋ฐ์ดํฐ ์์ง์ ๋ฒ์ ์ ์ฌ์ ๋์์ด ๋ฉ๋๋ค.
- AI ํ์ต ์ ํ: ์ฌ๋ฅ๋ท์ AI ์์ฑ ์ปจํ ์ธ ๋ฅผ ํ AI ๋ชจ๋ธ ํ์ต์ ๋ฌด๋จ ์ฌ์ฉํ๋ ํ์๋ ๊ธ์ง๋๋ฉฐ, ์ด๋ ์ง์ ์ฌ์ฐ๊ถ ์นจํด๋ก ๊ฐ์ฃผ๋ฉ๋๋ค.
์์ง ๋๊ธ์ด ์์ต๋๋ค.