Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import com.gbsw.snapy.domain.users.repository.UserRepository;
import com.gbsw.snapy.global.exception.CustomException;
import com.gbsw.snapy.global.exception.ErrorCode;
import com.gbsw.snapy.global.filter.BannedWordFilter;
import com.gbsw.snapy.global.security.jwt.JwtProperties;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
Expand All @@ -33,9 +34,13 @@ public class AuthService {
private final PasswordEncoder passwordEncoder;
private final JwtProvider jwtProvider;
private final JwtProperties jwtProperties;
private final BannedWordFilter bannedWordFilter;

@Transactional
public RegisterResponse register(RegisterRequest dto) {
if (bannedWordFilter.containsBannedWord(dto.getHandle())) {
throw new CustomException(ErrorCode.HANDLE_CONTAINS_BANNED_WORD);
}
if (userRepository.existsByHandle(dto.getHandle())) {
throw new CustomException(ErrorCode.DUPLICATE_HANDLE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
import com.gbsw.snapy.domain.streaks.service.StreakService;
import com.gbsw.snapy.global.exception.CustomException;
import com.gbsw.snapy.global.exception.ErrorCode;
import com.gbsw.snapy.global.filter.BannedWordFilter;
import com.gbsw.snapy.infra.s3.S3Service;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
Expand All @@ -36,6 +37,7 @@ public class UserService {
private final S3Service s3Service;
private final StreakService streakService;
private final PhoneVerificationService phoneVerificationService;
private final BannedWordFilter bannedWordFilter;

public UserProfileResponse getProfile(String handle, Long viewerId) {
User user = userRepository.findByHandleAndDeletedAtIsNull(handle)
Expand Down Expand Up @@ -145,12 +147,18 @@ public void deleteAccount(Long userId) {
}

public CheckHandleResponse checkHandle(String handle) {
if (bannedWordFilter.containsBannedWord(handle)) {
throw new CustomException(ErrorCode.HANDLE_CONTAINS_BANNED_WORD);
}
boolean available = !userRepository.existsByHandle(handle);
return new CheckHandleResponse(available);
}

@Transactional
public void updateHandle(Long userId, UpdateHandleRequest request) {
if (bannedWordFilter.containsBannedWord(request.getHandle())) {
throw new CustomException(ErrorCode.HANDLE_CONTAINS_BANNED_WORD);
}
if (userRepository.existsByHandle(request.getHandle())) {
throw new CustomException(ErrorCode.DUPLICATE_HANDLE);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public enum ErrorCode {
DELETED_USER(HttpStatus.NOT_FOUND, "탈퇴한 사용자입니다."),
INVALID_CREDENTIALS(HttpStatus.UNAUTHORIZED, "이메일 또는 비밀번호가 올바르지 않습니다."),
DUPLICATE_HANDLE(HttpStatus.CONFLICT, "이미 사용 중인 핸들입니다."),
HANDLE_CONTAINS_BANNED_WORD(HttpStatus.BAD_REQUEST, "사용할 수 없는 단어가 포함되어 있습니다."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "이미 사용 중인 이메일입니다."),
DUPLICATE_PHONE(HttpStatus.CONFLICT, "이미 사용 중인 전화번호입니다."),

Expand Down
44 changes: 44 additions & 0 deletions src/main/java/com/gbsw/snapy/global/filter/BannedWordFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.gbsw.snapy.global.filter;

import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.nio.charset.StandardCharsets;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@Component
public class BannedWordFilter {

private static final String BANNED_WORDS_PATH = "filter/banned-words.txt";

private Set<String> bannedWords;

@PostConstruct
public void init() throws IOException {
ClassPathResource resource = new ClassPathResource(BANNED_WORDS_PATH);
try (BufferedReader reader = new BufferedReader(
new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
bannedWords = reader.lines()
.map(String::trim)
.filter(line -> !line.isEmpty() && !line.startsWith("#"))
.map(String::toLowerCase)
.collect(Collectors.toUnmodifiableSet());
}
log.info("BannedWordFilter loaded {} words", bannedWords.size());
}
Comment on lines +24 to +35

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

IOException 전파로 인한 애플리케이션 시작 실패 가능성

@PostConstruct 메서드가 IOException을 throws하므로, banned-words.txt 파일이 누락되거나 읽을 수 없는 경우 애플리케이션 시작이 실패합니다. 개발 환경과 프로덕션 환경 간 배포 시 파일 누락 위험이 있습니다.

더 명확한 에러 메시지와 함께 초기화 실패를 처리하는 것을 권장합니다.

💡 개선된 에러 처리
 `@PostConstruct`
-public void init() throws IOException {
+public void init() {
+    try {
         ClassPathResource resource = new ClassPathResource(BANNED_WORDS_PATH);
+        if (!resource.exists()) {
+            log.error("Banned words file not found: {}", BANNED_WORDS_PATH);
+            throw new IllegalStateException("Required resource not found: " + BANNED_WORDS_PATH);
+        }
         try (BufferedReader reader = new BufferedReader(
                 new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
             bannedWords = reader.lines()
                     .map(String::trim)
                     .filter(line -> !line.isEmpty() && !line.startsWith("#"))
                     .map(String::toLowerCase)
                     .collect(Collectors.toUnmodifiableSet());
         }
         log.info("BannedWordFilter loaded {} words", bannedWords.size());
+    } catch (IOException e) {
+        log.error("Failed to load banned words from: {}", BANNED_WORDS_PATH, e);
+        throw new IllegalStateException("Failed to initialize BannedWordFilter", e);
+    }
 }
🤖 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/gbsw/snapy/global/filter/BannedWordFilter.java` around
lines 24 - 35, The init() method currently declares throws IOException so a
missing or unreadable BANNED_WORDS_PATH file can abort startup; change init() to
handle IO exceptions internally by removing the throws declaration, wrap the
ClassPathResource/BufferedReader logic in a try-catch(IOException |
RuntimeException), and on failure log a clear, descriptive error including the
exception (use log.error("... {}", e) or similar) and assign bannedWords to an
empty unmodifiable set (e.g., Collections.emptySet()) as a safe fallback so the
application can start; keep the log.info("BannedWordFilter loaded {} words",
bannedWords.size()) call after the try/catch.


public boolean containsBannedWord(String input) {
if (input == null || input.isBlank()) {
return false;
}
String normalized = input.toLowerCase().replaceAll("\\s+", "");
return bannedWords.stream().anyMatch(normalized::contains);
Comment on lines +41 to +42

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

치명적 오류: 단순 부분 문자열 매칭으로 인한 대규모 오탐 발생

String.contains()는 단어 경계를 고려하지 않는 부분 문자열 매칭을 수행하므로, 정상적인 단어도 차단됩니다:

  • "classic" → "ass" 포함으로 차단
  • "assessment" → "ass" 포함으로 차단
  • "expression" → "sex" 포함으로 차단
  • "basketball" → "ball" 포함으로 차단

특히 금칙어 목록에 "sex", "ass", "cum" 등 짧은 단어가 포함되어 있어 영향 범위가 매우 큽니다.

🔧 단어 경계를 고려한 정규식 기반 매칭 방식으로 개선
+import java.util.regex.Pattern;
+
 `@Slf4j`
 `@Component`
 public class BannedWordFilter {
 
     private static final String BANNED_WORDS_PATH = "filter/banned-words.txt";
 
-    private Set<String> bannedWords;
+    private Set<Pattern> bannedWordPatterns;
 
     `@PostConstruct`
     public void init() throws IOException {
         ClassPathResource resource = new ClassPathResource(BANNED_WORDS_PATH);
         try (BufferedReader reader = new BufferedReader(
                 new InputStreamReader(resource.getInputStream(), StandardCharsets.UTF_8))) {
-            bannedWords = reader.lines()
+            bannedWordPatterns = reader.lines()
                     .map(String::trim)
                     .filter(line -> !line.isEmpty() && !line.startsWith("#"))
                     .map(String::toLowerCase)
+                    .map(word -> Pattern.compile("\\b" + Pattern.quote(word) + "\\b", Pattern.CASE_INSENSITIVE))
-                    .collect(Collectors.toUnmodifiableSet());
+                    .collect(Collectors.toSet());
         }
-        log.info("BannedWordFilter loaded {} words", bannedWords.size());
+        log.info("BannedWordFilter loaded {} patterns", bannedWordPatterns.size());
     }
 
     public boolean containsBannedWord(String input) {
         if (input == null || input.isBlank()) {
             return false;
         }
-        String normalized = input.toLowerCase().replaceAll("\\s+", "");
-        return bannedWords.stream().anyMatch(normalized::contains);
+        // 공백 회피를 방지하기 위해 공백 제거 버전도 검사
+        String withSpaces = input.toLowerCase();
+        String withoutSpaces = withSpaces.replaceAll("\\s+", "");
+        
+        return bannedWordPatterns.stream()
+                .anyMatch(pattern -> pattern.matcher(withSpaces).find() 
+                                  || pattern.matcher(withoutSpaces).find());
     }
 }
🤖 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/gbsw/snapy/global/filter/BannedWordFilter.java` around
lines 41 - 42, The current BannedWordFilter uses normalized.contains(...) which
causes false positives by matching substrings (e.g., "classic" -> "ass"); update
the matching in the method that builds/checks bannedWords to use word-boundary,
case-insensitive regexes instead of simple substring checks: stop removing
whitespace with input.toLowerCase().replaceAll("\\s+",""), escape each banned
word (from the bannedWords collection) for regex, compile patterns like
(?i)\b<escaped_word>\b (or use Unicode-aware boundaries) and test the input
against those Patterns (in the same class/method where normalized and
bannedWords are used) so only whole-word matches trigger blocking. Ensure the
logic caches compiled Patterns to avoid recompiling per-check.

}
}
Loading