Skip to content

Commit fb8e51f

Browse files
committed
fix(user): Update reserved username and enforce check
- Add `String.isReversedUsername` helper function - Bump version to 0.5.3
1 parent b37ac5a commit fb8e51f

5 files changed

Lines changed: 111 additions & 15 deletions

File tree

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ plugins {
1010

1111
group = "me.loghub"
1212

13-
version = "0.5.2-SNAPSHOT"
13+
version = "0.5.3-SNAPSHOT"
1414

1515
java { toolchain { languageVersion = JavaLanguageVersion.of(21) } }
1616

src/main/kotlin/me/loghub/api/constant/message/ResponseMessage.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ object ResponseMessage {
5656
const val PRIVACY_UPDATE_SUCCESS = "정보 공개 설정이 성공적으로 업데이트되었습니다!"
5757
const val USERNAME_UPDATE_SUCCESS = "아이디가 성공적으로 변경되었습니다!"
5858
const val USERNAME_NOT_CHANGED = "유저네임이 변경되지 않았습니다."
59+
const val USERNAME_NOT_ALLOWED = "유저네임에 허용되지 않는 단어가 포함되어 있습니다."
5960
const val INVALID_DATE_RANGE = "날짜 범위가 유효하지 않습니다."
6061

6162
object GitHub {

src/main/kotlin/me/loghub/api/lib/validation/UsernameValidation.kt

Lines changed: 93 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,6 @@ import kotlin.reflect.KClass
99

1010
private const val USERNAME_REGEX = "[a-zA-Z0-9]+$"
1111

12-
// root, admin,
13-
// search, post, edit,
14-
// join, login, logout,
15-
// setting, settings, support, notifications,
16-
// article, articles, series, question, questions, topic, topics
17-
private const val USERNAME_RESERVED =
18-
"root|admin|search|post|edit|join|login|logout|setting|settings|support|notifications|article|articles|series|question|questions|topic|topics"
19-
private const val USERNAME_RESERVED_REGEX = "^(?!.*\\b($USERNAME_RESERVED)\\b).*$"
20-
2112
@Constraint(validatedBy = [])
2213
@Target(
2314
AnnotationTarget.FIELD,
@@ -28,10 +19,101 @@ private const val USERNAME_RESERVED_REGEX = "^(?!.*\\b($USERNAME_RESERVED)\\b).*
2819
@NotBlank(message = "유저네임은 필수 입력 항목입니다.")
2920
@Size(min = 4, max = 12, message = "유저네임은 4자 이상 12자 이하이어야 합니다.")
3021
@Pattern(regexp = USERNAME_REGEX, message = "유저네임은 영문 소문자와 숫자로만 입력해주세요.")
31-
@Pattern(regexp = USERNAME_RESERVED_REGEX, message = "유저네임에 사용할 수 없는 단어가 포함되어 있습니다.")
3222
@Trimmed
3323
annotation class UsernameValidation(
3424
val message: String = "잘못된 유저네임입니다.",
3525
val groups: Array<KClass<*>> = [],
3626
val payload: Array<KClass<out Payload>> = []
37-
)
27+
)
28+
29+
private val RESERVED_USERNAMES: Set<String> = setOf(
30+
// System / Admin
31+
"root", "admin", "administrator", "superadmin", "superuser", "owner", "master",
32+
"moderator", "mod", "staff", "team", "developers", "devs",
33+
34+
// Dashboard / Access
35+
"dashboard", "adminpanel", "system", "sys",
36+
"config", "configs", "configuration",
37+
"internal", "private", "public",
38+
39+
// API / Tech
40+
"api", "apis", "api-doc", "api-docs",
41+
"graphql", "rest", "soap", "rpc", "ws", "websocket",
42+
"static", "assets", "fonts", "icons", "images", "media",
43+
"file", "files", "upload", "uploads",
44+
"download", "downloads",
45+
46+
// Environments
47+
"dev", "prod", "test",
48+
49+
// Exec / Command injection
50+
"sh", "shell", "cmd", "command", "commands",
51+
"exec", "execute", "run", "eval",
52+
"php", "bin",
53+
"cdn", "cdn-cgi", "cgi",
54+
55+
// Security / Logs / Monitoring
56+
"secure", "security", "audit",
57+
"log", "logs", "error", "errors",
58+
"status", "statuspage",
59+
"health", "healthcheck",
60+
"metrics",
61+
"monitor", "monitoring",
62+
"trace", "debug", "debugger",
63+
64+
// Billing / Payment
65+
"billing", "payment", "payments",
66+
"subscribe", "subscription",
67+
68+
// Common pages
69+
"index", "home", "about", "contact", "help", "faq",
70+
"docs", "manual", "documentation",
71+
"terms", "privacy", "policy",
72+
"feed", "rss", "sitemap", "robots", "license",
73+
74+
// Auth / Account
75+
"auth", "authenticate", "authorize",
76+
"account", "accounts",
77+
"profile", "profiles",
78+
"user", "users", "my", "me", "self",
79+
"register", "signup", "signin",
80+
"join", "login", "logout",
81+
"password", "forgot", "reset",
82+
"verify", "confirm",
83+
"oauth", "oauth2", "callback",
84+
"session", "sessions",
85+
"token", "tokens",
86+
"setting", "settings",
87+
"legal", "support",
88+
89+
// Content routes
90+
"article", "articles",
91+
"series",
92+
"question", "questions",
93+
"topic", "topics",
94+
"tag", "tags",
95+
"comment", "comments",
96+
"search", "post", "edit",
97+
98+
// Additional conflict-prone words
99+
"new", "create", "update", "delete",
100+
"remove", "add", "list",
101+
"page", "pages",
102+
"reply", "replies",
103+
"inbox", "outbox",
104+
"message", "messages",
105+
"chat", "chats",
106+
"notification", "notifications",
107+
108+
// Anti-phishing
109+
"admin123", "support-team", "helpdesk",
110+
"service", "services",
111+
"official", "official-team",
112+
"secure-login",
113+
114+
// Domain
115+
"loghub",
116+
).map { it.lowercase() }.toSet()
117+
118+
fun String.isReservedUsername(): Boolean =
119+
this.lowercase() in RESERVED_USERNAMES

src/main/kotlin/me/loghub/api/service/auth/JoinService.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,12 +11,14 @@ import me.loghub.api.dto.task.mail.JoinMailSendRequest
1111
import me.loghub.api.entity.user.User
1212
import me.loghub.api.exception.auth.BadOTPException
1313
import me.loghub.api.lib.redis.key.RedisKeys
14+
import me.loghub.api.lib.validation.isReservedUsername
1415
import me.loghub.api.proxy.TaskAPIProxy
1516
import me.loghub.api.repository.user.UserRepository
1617
import me.loghub.api.service.auth.token.TokenService
1718
import me.loghub.api.service.common.MailService
1819
import me.loghub.api.util.OTPBuilder
1920
import me.loghub.api.util.checkConflict
21+
import me.loghub.api.util.checkField
2022
import org.springframework.data.redis.core.RedisTemplate
2123
import org.springframework.stereotype.Service
2224

@@ -54,6 +56,10 @@ class JoinService(
5456
}
5557

5658
private fun checkJoinable(email: String, username: String) {
59+
checkField(
60+
User::username.name,
61+
!username.isReservedUsername(),
62+
) { ResponseMessage.User.USERNAME_NOT_ALLOWED }
5763
checkConflict(
5864
User::email.name,
5965
userRepository.existsByEmail(email),

src/main/kotlin/me/loghub/api/service/user/UserService.kt

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,15 @@ import me.loghub.api.constant.message.ResponseMessage
44
import me.loghub.api.dto.user.UpdateUsernameDTO
55
import me.loghub.api.entity.user.User
66
import me.loghub.api.exception.entity.EntityNotFoundException
7+
import me.loghub.api.lib.validation.isReservedUsername
78
import me.loghub.api.mapper.user.UserMapper
89
import me.loghub.api.proxy.TaskAPIProxy
910
import me.loghub.api.repository.article.ArticleRepository
1011
import me.loghub.api.repository.question.QuestionRepository
1112
import me.loghub.api.repository.series.SeriesRepository
1213
import me.loghub.api.repository.user.UserRepository
1314
import me.loghub.api.util.checkConflict
15+
import me.loghub.api.util.checkField
1416
import me.loghub.api.util.requireNotEquals
1517
import org.springframework.security.core.userdetails.UsernameNotFoundException
1618
import org.springframework.stereotype.Service
@@ -32,18 +34,23 @@ class UserService(
3234

3335
@Transactional
3436
fun updateUsername(requestBody: UpdateUsernameDTO, user: User) {
37+
val (oldUsername, newUsername) = Pair(user.username, requestBody.newUsername)
38+
3539
requireNotEquals(
3640
UpdateUsernameDTO::newUsername.name,
37-
user.username, requestBody.newUsername,
41+
user.username, newUsername,
3842
) { ResponseMessage.User.USERNAME_NOT_CHANGED }
43+
checkField(
44+
UpdateUsernameDTO::newUsername.name,
45+
!newUsername.isReservedUsername(),
46+
) { ResponseMessage.User.USERNAME_NOT_ALLOWED }
3947
checkConflict(
4048
UpdateUsernameDTO::newUsername.name,
41-
userRepository.existsByUsernameIgnoreCase(requestBody.newUsername)
49+
userRepository.existsByUsernameIgnoreCase(newUsername)
4250
) { ResponseMessage.User.USERNAME_ALREADY_EXISTS }
4351

4452
val foundUser = userRepository.findByUsername(user.username)
4553
?: throw UsernameNotFoundException(ResponseMessage.User.NOT_FOUND)
46-
val (oldUsername, newUsername) = Pair(foundUser.username, requestBody.newUsername)
4754

4855
foundUser.updateUsername(newUsername)
4956
articleRepository.updateWriterUsernameByWriterUsername(oldUsername, newUsername)

0 commit comments

Comments
 (0)