Verizon DBIR 2024 보고서에 따르면 웹 애플리케이션 침해 사고의 77%가 탈취된 자격증명을 사용한 공격이다. Have I Been Pwned에 등록된 유출 계정 121억 개를 돌리는 Credential stuffing은 평균 0.5~2% 성공률이 나온다. 100만 건 시도에 최소 5,000개가 뚫린다는 뜻이다. OWASP ASVS V2 Authentication이 Brute force 방어를 필수 통제로 지정한 이유다.
"The use of authenticators that resist phishing and other attacks is one of the most effective security measures available." — NIST SP 800-63B Digital Identity Guidelines, §5.2.5
왜 로그인 엔드포인트가 1번 타깃인가
로그인 엔드포인트는 구조적으로 인증 없이 접근 가능해야 하고, 성공과 실패를 구분하는 응답을 반환해야 한다. Cloudflare Radar 집계 기준으로 전체 인터넷 트래픽의 약 30~40%가 자동화 봇에서 발생하며, 이 중 상당 비율이 인증 엔드포인트를 목표로 한다.
Brute force 차단은 어떻게 구현하나요
OWASP Authentication Cheat Sheet는 IP 기준과 계정 기준을 병행할 것을 명시한다.
실패 카운트와 IP 락 — Django + Redis 구현
import hashlib
from django.core.cache import cache
from django.http import JsonResponse
MAX_ATTEMPTS_BY_IP = 20
MAX_ATTEMPTS_BY_ACCOUNT = 5
LOCKOUT_SECONDS = 900
def check_brute_force(request, username):
ip = get_client_ip(request)
ip_key = f"bf:ip:{hashlib.sha256(ip.encode()).hexdigest()[:16]}"
acc_key = f"bf:acc:{hashlib.sha256(username.encode()).hexdigest()[:16]}"
if cache.get(ip_key, 0) >= MAX_ATTEMPTS_BY_IP or cache.get(acc_key, 0) >= MAX_ATTEMPTS_BY_ACCOUNT:
return False
return True
Rate Limit은 애플리케이션 레이어만으로 충분한가
충분하지 않다. Microsoft 보안팀이 발표한 데이터에 따르면 MFA + Rate limit 조합은 자동화 공격의 99.9%를 차단한다.
Nginx limit_req 설정
http {
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
server {
location /login {
limit_req zone=login_limit burst=3 nodelay;
limit_req_status 429;
proxy_pass http://django_app;
}
}
}
Cloudflare Rate Limiting Rule
# Expression
(http.request.uri.path contains "/login" and http.request.method eq "POST")
# Period: 60s / Requests: 10 / Action: Block
2FA/MFA 방식별 비교 — TOTP·SMS·FIDO2 중 뭐가 좋나요
FIDO Alliance 2024 보고서에 따르면 Passkey를 지원하는 서비스는 글로벌 기준 2023년 대비 2배 이상 증가했다.
| 방식 | 보안 수준 | 피싱 저항 | 구현 난이도 | 운영 비용 |
|---|---|---|---|---|
| TOTP | 높음 | 중간 | 낮음 | 없음 |
| Email OTP | 중간 | 낮음 | 낮음 | 발송 비용 |
| SMS OTP | 중간 | 낮음 | 중간 | SMS 비용 |
| FIDO2 / Passkey | 최고 | 높음 | 높음 | 없음 |
| 하드웨어 키 | 최고 | 높음 | 높음 | 키 구매비용 |
import pyotp
def generate_totp_secret():
return pyotp.random_base32()
def verify_totp(secret, token):
totp = pyotp.TOTP(secret)
return totp.verify(token, valid_window=1)
다 구현해놓고도 뚫리는 함정 4가지
1. 비밀번호 재설정 토큰 무한 유효
OWASP Forgot Password Cheat Sheet는 재설정 토큰 유효시간을 15분 이내로 제한하고, 사용 후 즉시 무효화할 것을 명시한다.
2. 세션 고정 (Session Fixation)
Django는 기본적으로 로그인 시 cycle_key()를 자동으로 호출해서 세션 ID를 교체한다. 직접 세션을 다루는 코드가 있다면 반드시 로그인 성공 직후 세션을 재발급해야 한다.
3. X-Forwarded-For 우회
TRUSTED_PROXIES = {"10.0.0.1", "10.0.0.2"}
def get_real_ip(request):
remote_addr = request.META.get("REMOTE_ADDR", "")
if remote_addr not in TRUSTED_PROXIES:
return remote_addr
xff = request.META.get("HTTP_X_FORWARDED_FOR", "")
return xff.split(",")[0].strip() if xff else remote_addr
4. 2FA 우회 — 비밀번호 재설정 백도어
OWASP ASVS V2.5.6 항목은 비밀번호 재설정 완료 후 모든 활성 세션을 강제 만료시킬 것을 요구한다. 보안 전문 기업 SENTRIX에서 웹 취약점 점검을 수행하다 보면 이 네 가지 패턴으로 우회가 되는 경우가 반복된다.
자주 묻는 질문
Brute force 차단을 IP 락으로만 해도 되나요?
충분하지 않습니다. NAT 환경에서는 정상 사용자가 같은 IP를 공유하기 때문에 한 명이 IP 락을 유발하면 나머지도 잠깁니다. IP 기준 + 계정 기준을 같이 적용해야 합니다.
SMS 2FA는 왜 권장하지 않나요?
SIM swap 공격에 취약합니다. 2019년 Twitter CEO 잭 도시 계정, 2020년 다수 암호화폐 거래소 계정이 SIM swap으로 뚫린 사례가 있습니다.
FIDO2/Passkey 구현은 복잡한가요?
Python의 py_webauthn 라이브러리가 WebAuthn 서버 로직 대부분을 처리해줍니다. 신규 서비스 기준으로 초기 구현에 2~3일이 현실적인 일정입니다.
Rate Limit 설정에서 합리적인 임계치는 얼마인가요?
OWASP Authentication Cheat Sheet 권고는 동일 IP 기준 분당 5~10회, 계정 기준 15분에 5회입니다.
Django 기본 인증을 쓰면 이 설정들이 자동으로 적용되나요?
아닙니다. Django 기본 인증 뷰는 Rate limit이나 Brute force 차단을 기본으로 제공하지 않습니다. django-axes 패키지를 추가하면 계정 잠금과 IP 차단을 설정으로 활성화할 수 있습니다.
로그인 페이지 직접 점검해보기
codescan.kr에서 로그인 페이지 URL을 입력하면 rate limit 미적용 여부, 인증 관련 헤더 누락, 세션 처리 이상 징후를 자동으로 탐지한다. 해킹대회 수상 경력의 보안 전문가가 설계한 룰셋 기반이다.