이 문서를 통해 아래 질문들에 답할 수 있게 됩니다.
- MFA는 어떤 공격을 줄이고, 어떤 공격은 여전히 남기는가?
- MFA fatigue, recovery code abuse, MFA 해제 공격을 어떻게 설계에서 막는가?
- Spring 백엔드에서 MFA 검증 상태를 세션과 감사 로그에 어떻게 반영하는가?
개요
MFA는 비밀번호가 유출되었을 때 계정 탈취를 어렵게 만드는 두 번째 방어선이다.
하지만 MFA가 있으면 비밀번호 정책, 로그인 실패 제한, 세션 보호를 느슨하게 해도 된다는 뜻은 아니다.
MFA 자체도 공격면이다. 등록, 재등록, 해제, recovery code 발급, push 승인, remember device가 모두 민감한 상태 변경이다.
공격 시나리오
공격자는 credential stuffing으로 사용자의 비밀번호를 맞혔다.
로그인 후 MFA 단계에서 다음을 시도한다.
- push 알림을 계속 보내 사용자가 실수로 승인하게 만든다.
mfaPassed=true같은 클라이언트 값을 조작한다.- recovery code 입력 실패를 무제한 반복한다.
- MFA 해제 API를 호출하면서 “이미 로그인했으니 충분하다”는 설계를 이용한다.
- MFA 등록 단계에서 자신의 기기를 추가한다.
MFA는 로그인 뒤의 한 화면이 아니라 계정 보안 상태를 바꾸는 기능들의 묶음이다.
취약한 요청/응답 예시
POST /login/mfa/verify HTTP/1.1
Host: api.example.com
Cookie: SESSION=pre-auth-session
Content-Type: application/json
{
"accountId": 1004,
"mfaPassed": true,
"method": "push",
"rememberDevice": true
}HTTP/1.1 200 OK
Set-Cookie: JSESSIONID=full-session; Path=/; HttpOnly; Secure
Content-Type: application/json
{"message":"login success"}서버가 MFA 결과를 클라이언트 body에서 받으면 MFA는 방어선이 아니다. challenge id와 서버 저장 상태로 검증해야 한다.
주제별 핵심 판단
| 방식 | 장점 | 주의점 |
|---|---|---|
| SMS OTP | 도입이 쉽다 | SIM swap, 문자 탈취, 번호 재사용 위험이 있다 |
| TOTP | 오프라인 검증이 가능하다 | seed 유출, phishing, 시간 오차 처리가 필요하다 |
| Push | 사용자 경험이 좋다 | fatigue 공격과 잘못된 승인 위험이 있다 |
| Recovery code | 계정 복구에 필요하다 | 저장, 재발급, 사용 로그가 민감하다 |
| WebAuthn/passkey | phishing 저항성이 높다 | 기기 등록과 복구 UX를 설계해야 한다 |
계정 위험도와 운영 비용에 따라 선택하되, 관리자와 결제 권한에는 더 강한 방식이나 step-up을 요구하는 편이 안전하다.
개선된 요청/응답 예시
MFA 설계의 핵심은 “어떤 상태에서 어떤 action을 허용하는가”다.
- 개선된 MFA 흐름은 로그인 성공 직후가 아니라 위험도에 따라 민감 작업 앞에서도 step-up을 요구한다.
- MFA 실패와 재시도는 계정별, 기기별, challenge별로 기록해 피로 공격을 탐지한다.
방어 설계
password_verified_session:
- allowed: MFA challenge 요청, logout
- denied: 결제, 개인정보 조회, 권한 변경
mfa_verified_session:
- allowed: 일반 서비스 이용
- step-up required: MFA 해제, recovery code 재발급, 관리자 권한 변경MFA challenge는 서버가 만든다.
- challenge id를 발급한다.
- account id, method, expiry, attempt count, device signal을 서버에 저장한다.
- push는 횟수와 재시도 간격을 제한한다.
- TOTP는 짧은 시간 창만 허용하고 재사용을 막는다.
- recovery code는 해시로 저장하고 사용 후 폐기한다.
Spring/HTTP 예시
@PostMapping("/login/mfa/challenges")
public ResponseEntity<MfaChallengeResponse> createChallenge(
@AuthenticationPrincipal PreAuthPrincipal principal,
HttpServletRequest request) {
MfaChallenge challenge = mfaChallengeService.create(
principal.accountId(),
ClientSignal.from(request));
return ResponseEntity.accepted().body(MfaChallengeResponse.from(challenge));
}@PostMapping("/login/mfa/verify")
public ResponseEntity<Void> verifyMfa(
@AuthenticationPrincipal PreAuthPrincipal principal,
@Valid @RequestBody MfaVerifyRequest request,
HttpServletRequest servletRequest) {
MfaResult result = mfaChallengeService.verify(
principal.accountId(),
request.challengeId(),
request.code(),
ClientSignal.from(servletRequest));
sessionUpgradeService.upgradeToMfaVerifiedSession(principal.sessionId(), result);
return ResponseEntity.noContent().build();
}MFA 해제는 일반 로그인 세션만으로 허용하지 않는다.
public void disableMfa(AccountPrincipal principal, DisableMfaCommand command) {
if (!principal.mfaVerified() || !stepUpVerifier.verifiedRecently(principal.accountId())) {
audit.mfaChangeDenied(principal.accountId(), "step_up_required");
throw new AccessDeniedException("step-up required");
}
mfaRepository.disable(principal.accountId(), command.methodId());
sessionRegistry.revokeOtherSessions(principal.accountId());
audit.mfaDisabled(principal.accountId(), command.methodId());
}운영 로그와 감사 지점
level=INFO event=mfa_challenge_created result=success account_id=1004 method=totp challenge_id_hash=78d2 ip=203.0.113.10 trace_id=71ac7e01level=WARN event=mfa_verify_failed result=denied account_id=1004 method=push reason=too_many_push_requests attempt_count=5 trace_id=71ac7e02level=WARN event=mfa_disabled result=success account_id=1004 method=totp step_up=true other_sessions_revoked=3 trace_id=71ac7e03MFA secret, TOTP seed, recovery code 원문은 로그에 남기지 않는다.
실전 판단 기준
mfaPassed=true,accountId조작으로 MFA를 통과할 수 없는가?- MFA가 있다고 비밀번호 재사용, session 탈취, recovery code 유출 위험이 사라지는 것은 아니다.
- MFA 등록/해제/복구는 로그인보다 더 강한 감사 로그와 재인증을 요구한다.
테스트 포인트
- pre-auth session으로 개인정보 조회나 결제 API를 호출하면 거부되는가?
- push challenge 반복 요청에 rate limit가 있는가?
- recovery code는 해시 저장되고 사용 후 폐기되는가?
- MFA 해제와 recovery code 재발급에 최근 step-up이 필요한가?
- MFA 등록, 해제, 실패, recovery code 사용이 일반 로그인 로그와 구분되는가?
위험 신호!
- MFA 통과 여부를 클라이언트가 보낸 boolean으로 판단한다.
- push 알림을 무제한 보낼 수 있다.
- recovery code 원문을 DB나 로그에 저장한다.
- MFA 해제에 재인증이나 step-up이 없다.
- MFA가 필요한 상태에서도 완전한 세션 권한을 발급한다.
- 관리자와 일반 사용자의 MFA 정책이 동일하다.
확인 질문
확인 질문
- MFA가 막는 공격과 막지 못하는 공격은 무엇인가?
- 비밀번호 탈취 후 즉시 로그인되는 공격을 줄이지만, phishing, MFA fatigue, recovery code 탈취, 세션 탈취는 별도로 방어해야 한다.
- MFA challenge를 서버 상태로 관리해야 하는 이유는 무엇인가?
- 클라이언트가 MFA 성공 여부를 조작할 수 없게 하고, 만료와 시도 횟수, 재사용 여부를 통제하기 위해서다.
- MFA 해제 로그가 중요한 이유는 무엇인가?
- 계정 탈취자가 가장 먼저 두 번째 방어선을 제거하려 할 수 있기 때문이다.