숨겨진 필드와 작별: 토큰 없는 최신 CSRF 보호
Source: Dev.to
크로스‑사이트 요청 위조(CSRF) 공격에 대한 방어를 설정해 본 적이 있다면, 아마도 다음과 같은 절차를 기억할 것입니다: 고유 토큰을 생성하고, 이를 숨겨진 폼 필드에 삽입하며, 스크립트가 올바른 헤더를 전송하도록 보장하는 것이죠. 2025년에는 이 과정을 크게 단순화하면서 코드도 훨씬 깔끔하게 만들 수 있습니다.
전통적인 CSRF 토큰
- 상태 동기화 – 토큰은 서버와 클라이언트가 공유 상태를 유지하도록 요구하여 페이지 캐싱을 복잡하게 만들었습니다.
- 마크업 부피 증가 – 모든 폼에 추가 숨김 입력이 필요했습니다.
- 디버깅 어려움 – 만료된 토큰 오류가 귀중한 개발 시간을 소모했습니다.
이러한 단점들 때문에 토큰 기반 보호는 피할 수 없는 기술 부채처럼 느껴졌습니다.
Fetch Metadata를 이용한 브라우저 기반 보호
Modern browsers now transmit a special header known as Fetch Metadata. The most critical element is the Sec-Fetch-Site header, which tells the server the true origin of a request.
| 요청 출처 | 헤더 값 |
|---|---|
| 동일 출처 폼 제출 | Sec-Fetch-Site: same-origin |
| 크로스 사이트 요청 (예: 악성 사이트) | Sec-Fetch-Site: cross-site |
브라우저는 이 헤더가 JavaScript를 통해 위조되거나 수정될 수 없음을 보장하므로, 서버는 이를 완전히 신뢰할 수 있습니다.
구형 브라우저를 위한 대체 방안
브라우저가 Fetch Metadata를 전송하지 않을 경우, Origin 또는 Referer 헤더를 확인하는 방식으로 대체할 수 있습니다. same-origin 또는 none(예: 직접 URL 입력)인 요청은 허용하고, 그 외의 요청은 거부합니다.
구현 예시
아래는 상태를 변경하는 메서드에 대해 교차 사이트 요청을 거부하는 Node.js (Express)와 Python (Flask)의 최소 예시입니다.
Express (Node.js)
// app.js
const express = require('express');
const app = express();
function csrfProtection(req, res, next) {
const method = req.method.toUpperCase();
const unsafeMethods = ['POST', 'PUT', 'PATCH', 'DELETE'];
if (!unsafeMethods.includes(method)) {
return next(); // Safe method, no check needed
}
const site = req.get('Sec-Fetch-Site');
if (site && site === 'cross-site') {
return res.status(403).send('Forbidden: CSRF protection');
}
// Fallback for older browsers
const origin = req.get('Origin') || req.get('Referer');
if (origin && !origin.includes(req.get('Host'))) {
return res.status(403).send('Forbidden: CSRF protection');
}
next();
}
app.use(express.json());
app.use(csrfProtection);
app.post('/api/data', (req, res) => {
// Handle state‑changing request
res.json({ status: 'success' });
});
app.listen(3000, () => console.log('Server running on port 3000'));
Flask (Python)
# app.py
from flask import Flask, request, abort, jsonify
app = Flask(__name__)
def csrf_protect():
unsafe_methods = {'POST', 'PUT', 'PATCH', 'DELETE'}
if request.method not in unsafe_methods:
return # Safe method
site = request.headers.get('Sec-Fetch-Site')
if site == 'cross-site':
abort(403, description='Forbidden: CSRF protection')
# Fallback for older browsers
origin = request.headers.get('Origin') or request.headers.get('Referer')
if origin and request.host not in origin:
abort(403, description='Forbidden: CSRF protection')
app.before_request(csrf_protect)
@app.route('/api/data', methods=['POST'])
def handle_data():
# Process the request
return jsonify(status='success')
if __name__ == '__main__':
app.run(port=5000)
헤더 기반 CSRF 보호의 장점
- 토큰 생성 및 저장 불필요 – 세션 측 상태를 없애고 마크업을 줄입니다.
- 캐시 친화적 – 오래된 토큰에 대해 걱정할 필요 없이 폼을 캐시할 수 있습니다.
- 디버깅 간소화 – 오류가 표준 HTTP 상태 코드로 축소됩니다.
- 성능 향상 – 전송되는 바이트 수가 감소하고 서버 측 처리량이 줄어듭니다.
Industry Recognition
OWASP 프로젝트는 OWASP 프로젝트가 Fetch Metadata를 기존의 classic anti‑CSRF 토큰에 대한 실현 가능한 대안으로 인정했으며, 이를 실험적 상태에서 권장 실천으로 전환했습니다.
결론
브라우저는 점점 더 똑똑해져서 일상적인 보안 작업을 자동으로 처리하고 있습니다. CSRF 보호를 숨겨진 필드와 토큰에서 신뢰할 수 있는 요청 메타데이터로 전환함으로써, 더 깔끔하고 가벼운 코드베이스에서도 동일한 수준의 보안을 달성할 수 있습니다. 이는 더 단순하고 유지보수가 쉬운 웹을 향한 한 걸음입니다.