숨겨진 필드와 작별: 토큰 없는 최신 CSRF 보호

발행: (2025년 12월 26일 오전 01:02 GMT+9)
6 min read
원문: Dev.to

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 보호를 숨겨진 필드와 토큰에서 신뢰할 수 있는 요청 메타데이터로 전환함으로써, 더 깔끔하고 가벼운 코드베이스에서도 동일한 수준의 보안을 달성할 수 있습니다. 이는 더 단순하고 유지보수가 쉬운 웹을 향한 한 걸음입니다.

Back to Blog

관련 글

더 보기 »

JSON Web Token (JWT) 이해하기

어떤 시점에 웹 애플리케이션을 만들 때, 시스템을 위한 인증 솔루션을 개발해야 합니다. 이를 위한 다양한 전략이 있으며, 예를 들어…