Skip to content

♻️ refactor: SMS 인증 무차별 대입 방어 추가#137

Merged
komascode merged 6 commits into
developfrom
Refactor/#136
Jul 9, 2026
Merged

♻️ refactor: SMS 인증 무차별 대입 방어 추가#137
komascode merged 6 commits into
developfrom
Refactor/#136

Conversation

@komascode

@komascode komascode commented Jul 8, 2026

Copy link
Copy Markdown
Collaborator

📍 PR 타입 (하나 이상 선택)

  • 기능 추가
  • 버그 수정
  • 의존성, 환경 변수, 빌드 관련 코드 업데이트
  • 기타 사소한 수정

❗️ 관련 이슈 링크

Closed #136

📌 개요

  • 인증 로직 점검 중 발견된 보안 이슈를 개선했습니다.
  • SMS 인증번호(/auth/sms/verify)에 시도 횟수 제한이 없어, 5분 유효창 내에 6자리 코드(100만 조합)를 무차별 대입할 수 있었습니다. 실패 카운터를 추가해 이를 차단합니다.

🔁 변경 사항

  • SMS 무차별 대입 방어
  • 코드 불일치 5회 초과 시 해당 인증번호를 무효화 → 새 코드 재요청 유도(발송 60초 쿨다운으로 대입 차단)
  • 인증 성공 시 카운터 초기화, 불일치 시 남은 시도 횟수(remainingAttempts) 응답
  • 로그 노출 제거: sendSms 발송 성공 로그에서 인증번호(code)를 제거했습니다. (운영 로그 유출 방지)
  • 주석 정정: verification token은 TTL(10분) 동안 재사용 가능한 의도된 설계임을 명확히 했습니다. (동작 변경 없음)
  • 테스트: verifySms 분기 로직 단위 테스트 4종 추가 (성공 / 코드 만료 / 한도 미만 / 한도 초과)

📸 스크린샷

👀 기타 더 이야기해볼 점

  • 실패 한도(5회)와 카운터 TTL(코드 수명 5분)은 기존 로그인 실패 잠금 정책과 통일했습니다.

✅ 체크 리스트

  • PR 템플릿에 맞추어 작성했어요.
  • 변경 내용에 대한 테스트를 진행했어요.
  • 프로그램이 정상적으로 동작해요.
  • PR에 적절한 라벨을 선택했어요.
  • 불필요한 코드는 삭제했어요.

Summary by CodeRabbit

  • New Features

    • SMS 인증 실패 횟수 제한이 추가되어, 반복 실패 시 인증번호 재요청이 필요합니다.
    • 인증 성공 시 발급되는 검증 토큰의 사용 안내가 더 명확해졌습니다.
  • Bug Fixes

    • 인증번호가 만료되었을 때 적절한 오류가 반환됩니다.
    • 인증 실패 시 남은 시도 횟수가 함께 안내됩니다.
    • 인증 성공 후에는 관련 인증 정보가 정상적으로 초기화됩니다.
  • Tests

    • SMS 인증 성공/실패/만료/횟수 초과 흐름에 대한 검증이 추가되었습니다.

@komascode komascode self-assigned this Jul 8, 2026
@komascode komascode added ♻️ refactor 리팩토링! 🐛 bug 버그 수정! labels Jul 8, 2026
@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 69cffd18-6b14-447d-8afb-7447d36a0883

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

SMS 인증 검증에 Redis 기반 실패 카운터를 도입하여 시도 횟수 초과 시 코드를 무효화하는 브루트포스 방어 기능을 추가했다. 새 에러 코드, RedisUtil의 원자적 increment 메서드, 로그 마스킹, 주석 정정, 관련 단위 테스트가 포함되었다.

Changes

SMS 인증 브루트포스 방어

Layer / File(s) Summary
에러 코드 및 Redis 증가 유틸 추가
AuthErrorCode.java, RedisUtil.java
VERIFICATION_ATTEMPTS_EXCEEDED 에러 코드(429, AUTH429_2)를 추가하고, RedisUtil에 키 검증 후 원자적으로 값을 증가시키고 최초 생성 시 TTL을 설정하는 increment 메서드를 추가했다.
verifySms 실패 카운터 로직 구현
AuthService.java
SMS_VERIFY_FAIL_PREFIX, MAX_VERIFY_ATTEMPTS 상수를 도입하고, 코드 불일치 시 Redis 실패 카운터를 증가시켜 한도 도달 시 인증 코드/카운터를 삭제 후 예외 발생, 한도 미만 시 남은 시도 횟수를 포함한 예외를 반환하도록 변경했다. 성공 시 인증 코드와 실패 카운터 키를 모두 삭제한다.
로그 마스킹 및 주석 정정
AuthService.java
sendSms 성공 로그에서 인증 코드를 제거해 휴대폰 번호만 기록하고, validateVerificationTokenresetPassword의 토큰 재사용 관련 주석을 정정했다.
verifySms 시나리오 테스트
AuthServiceTest.java
성공, 만료, 한도 미만 불일치, 한도 초과 4가지 시나리오에 대해 예외 코드, 바인딩 값, Redis 키 삭제/증가 호출 여부를 검증하는 테스트를 신규 추가했다.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AuthService
  participant RedisUtil

  Client->>AuthService: verifySms(전화번호, 코드)
  AuthService->>RedisUtil: get(인증코드 키)
  alt 코드 없음
    AuthService-->>Client: AuthException(VERIFICATION_CODE_EXPIRED)
  else 코드 존재
    alt 코드 일치
      AuthService->>RedisUtil: delete(인증코드/실패카운터 키)
      AuthService-->>Client: 검증 성공 응답
    else 코드 불일치
      AuthService->>RedisUtil: increment(실패 카운터 키)
      RedisUtil-->>AuthService: 누적 실패 횟수
      alt 한도 도달
        AuthService->>RedisUtil: delete(인증코드/실패카운터 키)
        AuthService-->>Client: AuthException(VERIFICATION_ATTEMPTS_EXCEEDED)
      else 한도 미만
        AuthService-->>Client: AuthException(INVALID_VERIFICATION_CODE, 남은 시도 횟수)
      end
    end
  end
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed 제목이 SMS 인증 무차별 대입 방어 추가라는 핵심 변경을 간결하게 잘 요약합니다.
Description check ✅ Passed 템플릿의 주요 섹션과 체크리스트를 대부분 채워 변경 배경과 내용을 충분히 설명합니다.
Linked Issues check ✅ Passed SMS 무차별 대입 방어, 로그의 인증번호 제거, 토큰 주석 정정 등 이슈의 핵심 요구사항이 반영되었습니다.
Out of Scope Changes check ✅ Passed Redis 증분 헬퍼, 인증 흐름 수정, 로그 정리, 테스트 추가 모두 SMS 인증 보안 개선 범위 안에 있습니다.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch Refactor/#136

Comment @coderabbitai help to get the list of available commands.

@komascode komascode changed the title Refactor/#136 ♻️ refactor: SMS 인증 무차별 대입 방어 추가 Jul 8, 2026
@rlawngjs0313

Copy link
Copy Markdown
Member

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 8, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/test/java/com/example/scoi/domain/auth/service/AuthServiceTest.java (2)

93-112: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

한도 미만 경계값(4) 테스트 추가 권장.

현재 mismatchUnderLimit()는 failCount=2로 임의 값을 사용합니다. >= 비교의 정확한 경계(한도 5 바로 아래인 failCount=4, remainingAttempts=1)를 별도로 검증하면 off-by-one 회귀를 더 확실히 방지할 수 있습니다.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/example/scoi/domain/auth/service/AuthServiceTest.java`
around lines 93 - 112, Add a dedicated boundary test for the
`AuthServiceTest.mismatchUnderLimit` path that uses `authService.verifySms(...)`
with the failure counter at 4, not 2, so the `INVALID_VERIFICATION_CODE` branch
is validated exactly at the `>= 5` cutoff. Reuse the same `redisUtil.get`,
`redisUtil.increment`, and `verify(redisUtil, never())` assertions, and
additionally assert that `remainingAttempts` is 1 to cover the off-by-one
boundary in `verifySms`.

60-76: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

성공 테스트에 토큰 저장 검증 추가 권장.

success()는 응답 값과 코드/실패카운터 삭제만 검증하고, redisUtil.set(tokenKey, ...) 호출(검증 토큰의 Redis 저장)은 검증하지 않습니다. 토큰 영속화는 이 인증 흐름의 핵심 부분이므로, 키 구성(VERIFICATION_PREFIX + token)과 저장되는 전화번호 값을 함께 검증하면 회귀를 더 잘 잡을 수 있습니다.

♻️ 제안 diff
             AuthResDTO.SmsVerifyResponse response = authService.verifySms(request(CODE));

             // then
             assertThat(response.verificationToken()).isEqualTo("verification-token");
             assertThat(response.isExistingMember()).isTrue();
             verify(redisUtil).delete(SMS_KEY);   // 코드 삭제
             verify(redisUtil).delete(FAIL_KEY);  // 실패 카운터 초기화
+            verify(redisUtil).set(eq("verification:verification-token"), eq(PHONE), anyLong(), eq(TimeUnit.MINUTES));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/test/java/com/example/scoi/domain/auth/service/AuthServiceTest.java`
around lines 60 - 76, `AuthServiceTest.success()` only checks the response and
Redis deletions, but it misses the token persistence step in
`AuthService.verifySms`. Update the success test to also verify
`redisUtil.set(...)` is called with the verification-token-derived Redis key
(using the `VERIFICATION_PREFIX` + token pattern) and the expected phone number
value. Keep the existing assertions for `jwtUtil.createVerificationToken`,
`redisUtil.delete`, and `memberRepository.existsByPhoneNumber`, and use the
`success()` test plus `verifySms` as the main reference points.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/main/java/com/example/scoi/global/redis/RedisUtil.java`:
- Around line 61-70: The Redis increment logic in RedisUtil.increment currently
sets the TTL with a separate expire call after INCR, so a failure between those
steps can leave the counter without expiration. Update increment to make the
increment-and-expire flow atomic, either by wrapping the operations in a Lua
script or a transaction, or by explicitly handling expire failures and
retrying/raising as appropriate. Keep the fix centered on RedisUtil.increment
and preserve the existing validateInput and logging behavior.

---

Nitpick comments:
In `@src/test/java/com/example/scoi/domain/auth/service/AuthServiceTest.java`:
- Around line 93-112: Add a dedicated boundary test for the
`AuthServiceTest.mismatchUnderLimit` path that uses `authService.verifySms(...)`
with the failure counter at 4, not 2, so the `INVALID_VERIFICATION_CODE` branch
is validated exactly at the `>= 5` cutoff. Reuse the same `redisUtil.get`,
`redisUtil.increment`, and `verify(redisUtil, never())` assertions, and
additionally assert that `remainingAttempts` is 1 to cover the off-by-one
boundary in `verifySms`.
- Around line 60-76: `AuthServiceTest.success()` only checks the response and
Redis deletions, but it misses the token persistence step in
`AuthService.verifySms`. Update the success test to also verify
`redisUtil.set(...)` is called with the verification-token-derived Redis key
(using the `VERIFICATION_PREFIX` + token pattern) and the expected phone number
value. Keep the existing assertions for `jwtUtil.createVerificationToken`,
`redisUtil.delete`, and `memberRepository.existsByPhoneNumber`, and use the
`success()` test plus `verifySms` as the main reference points.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 20c88764-8003-4878-8aea-46f6a77d100b

📥 Commits

Reviewing files that changed from the base of the PR and between 4d6be6f and 7569673.

📒 Files selected for processing (4)
  • src/main/java/com/example/scoi/domain/auth/exception/code/AuthErrorCode.java
  • src/main/java/com/example/scoi/domain/auth/service/AuthService.java
  • src/main/java/com/example/scoi/global/redis/RedisUtil.java
  • src/test/java/com/example/scoi/domain/auth/service/AuthServiceTest.java

Comment on lines +61 to +70
public long increment(String key, long timeout, TimeUnit unit) {
validateInput(key);

Long count = redisTemplate.opsForValue().increment(key);
if (count != null && count == 1L) {
redisTemplate.expire(key, timeout, unit);
}
log.debug("Redis 증가: key={}, count={}", key, count);
return count == null ? 0L : count;
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# RedisTemplate<String,String> 빈 및 value serializer 설정 확인
rg -nP -C3 'RedisTemplate<\s*String\s*,\s*String\s*>|setValueSerializer|StringRedisSerializer|GenericJackson2JsonRedisSerializer' --type java

Repository: UMCSCOI/Backend

Length of output: 153


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== Redis-related Java files ==\n'
git ls-files 'src/main/java/**/*.java' | rg 'Redis|redis' || true

printf '\n== RedisUtil outline ==\n'
fd -a 'RedisUtil.java' src/main/java || true

printf '\n== Redis config/search ==\n'
rg -n -C 3 'RedisTemplate|RedisConnectionFactory|StringRedisSerializer|GenericJackson2JsonRedisSerializer|opsForValue\(\)\.increment|expire\(' src/main/java || true

Repository: UMCSCOI/Backend

Length of output: 6670


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== RedisUtil.java relevant lines ==\n'
sed -n '1,120p' src/main/java/com/example/scoi/global/redis/RedisUtil.java | cat -n

printf '\n== increment() call sites ==\n'
rg -n -C 2 '\.increment\(' src/main/java || true

Repository: UMCSCOI/Backend

Length of output: 4603


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== AuthService increment usage ==\n'
sed -n '110,160p' src/main/java/com/example/scoi/domain/auth/service/AuthService.java | cat -n

Repository: UMCSCOI/Backend

Length of output: 2794


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== SMS prefixes and send flow ==\n'
rg -n -C 3 'SMS_VERIFY_FAIL_PREFIX|cooldownKey|failKey|verificationCode|SMS_EXPIRATION_MINUTES|sendSms\(' src/main/java/com/example/scoi/domain/auth/service/AuthService.java || true

Repository: UMCSCOI/Backend

Length of output: 4116


INCR 뒤 TTL을 원자적으로 보장하세요. StringRedisSerializer가 사용되어 직렬화 문제는 없지만, count == 1L일 때만 expire를 별도 호출하면 중간 실패 시 실패 카운터가 TTL 없이 남을 수 있습니다. expire 실패를 처리하거나 Lua/트랜잭션으로 묶어 주세요.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/main/java/com/example/scoi/global/redis/RedisUtil.java` around lines 61 -
70, The Redis increment logic in RedisUtil.increment currently sets the TTL with
a separate expire call after INCR, so a failure between those steps can leave
the counter without expiration. Update increment to make the
increment-and-expire flow atomic, either by wrapping the operations in a Lua
script or a transaction, or by explicitly handling expire failures and
retrying/raising as appropriate. Keep the fix centered on RedisUtil.increment
and preserve the existing validateInput and logging behavior.

@kingmingyu kingmingyu left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

인증 횟수 제한 좋은 것 같습니다! 고생하셨습니다!

@rlawngjs0313

Copy link
Copy Markdown
Member

@komascode 이제부터 코드리뷰를 코드래빗이 대신 해줄거예요! 해당 리뷰 보고 수정할 사항 있다면 수정하고 별도로 없고 CI 파이프라인 통과하면 바로 머지해도 됩니다~!

@komascode

Copy link
Copy Markdown
Collaborator Author

코멘트 주신 거 확인했습니다! 감사합니다

komascode and others added 2 commits July 9, 2026 20:20
INCR과 EXPIRE가 별도 왕복이라 중간 실패 시 TTL 없는 카운터가 남을 수 있었다.
두 명령을 단일 스크립트로 묶어 한 번의 왕복으로 실행한다.

MULTI/EXEC는 INCR 반환값을 읽어 EXPIRE 여부를 분기할 수 없어 사용하지 않았다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
sendSms가 failKey를 지우지 않아 실패 카운터가 코드 수명을 넘어 이월됐다.
카운터 TTL은 첫 실패 기준, 코드 TTL은 발급 기준이라 두 창이 정렬되지 않아
4회 실패 후 새 코드를 받으면 오답 한 번에 즉시 무효화됐다.

새 코드에 새 시도 예산을 부여한다. 쿨다운(1분)이 재발급 남용을 막는다.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@komascode komascode merged commit 0d3e58f into develop Jul 9, 2026
2 checks passed
@komascode komascode deleted the Refactor/#136 branch July 9, 2026 11:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

🐛 bug 버그 수정! ♻️ refactor 리팩토링!

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants