From 888266bc02153924da803809877557cfb931cd62 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:29:34 +0000 Subject: [PATCH 1/5] Initial plan From 17f8d33ae4caacea23011c77248b3fe05989056d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:32:27 +0000 Subject: [PATCH 2/5] Fix critical security vulnerabilities: WebSocket CORS, session fixation, and authorization bypass Co-authored-by: RobotHanzo <36107150+RobotHanzo@users.noreply.github.com> --- .../robothanzo/werewolf/config/SessionConfig.java | 12 ++++++++++++ .../werewolf/config/WebSocketConfig.java | 5 ++++- .../werewolf/controller/AuthController.java | 12 ++++++++++-- .../robothanzo/werewolf/utils/IdentityUtils.java | 14 +++++++++++++- 4 files changed, 39 insertions(+), 4 deletions(-) diff --git a/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java b/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java index 632b78d..489ff7a 100644 --- a/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java +++ b/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java @@ -1,9 +1,21 @@ package dev.robothanzo.werewolf.config; import org.mongodb.spring.session.config.annotation.web.http.EnableMongoHttpSession; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.session.web.http.CookieSerializer; +import org.springframework.session.web.http.DefaultCookieSerializer; @Configuration @EnableMongoHttpSession(collectionName = "http_sessions") public class SessionConfig { + + @Bean + public CookieSerializer cookieSerializer() { + DefaultCookieSerializer serializer = new DefaultCookieSerializer(); + serializer.setSameSite("Lax"); + serializer.setUseHttpOnlyCookie(true); + serializer.setUseSecureCookie(!"http://localhost:5173".equals(System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173"))); + return serializer; + } } diff --git a/src/main/java/dev/robothanzo/werewolf/config/WebSocketConfig.java b/src/main/java/dev/robothanzo/werewolf/config/WebSocketConfig.java index 6223483..166af46 100644 --- a/src/main/java/dev/robothanzo/werewolf/config/WebSocketConfig.java +++ b/src/main/java/dev/robothanzo/werewolf/config/WebSocketConfig.java @@ -20,6 +20,9 @@ public WebSocketConfig(GlobalWebSocketHandler globalWebSocketHandler) { public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) { registry.addHandler(globalWebSocketHandler, "/ws") .addInterceptors(new org.springframework.web.socket.server.support.HttpSessionHandshakeInterceptor()) - .setAllowedOrigins("*"); + .setAllowedOrigins( + "http://localhost:5173", + "https://wolf.robothanzo.dev", + "http://wolf.robothanzo.dev"); } } diff --git a/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java b/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java index 91336eb..20d8aef 100644 --- a/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java +++ b/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java @@ -7,6 +7,7 @@ import io.mokulu.discord.oauth.DiscordOAuth; import io.mokulu.discord.oauth.model.TokensResponse; import io.mokulu.discord.oauth.model.User; +import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; @@ -41,13 +42,20 @@ public void login(@RequestParam(name = "guild_id", required = false) String guil } @GetMapping("/callback") - public void callback(@RequestParam String code, @RequestParam String state, HttpSession session, - HttpServletResponse response) throws IOException { + public void callback(@RequestParam String code, @RequestParam String state, + HttpServletRequest request, HttpServletResponse response) throws IOException { try { TokensResponse tokenResponse = discordOAuth.getTokens(code); DiscordAPI discordAPI = new DiscordAPI(tokenResponse.getAccessToken()); User user = discordAPI.fetchUser(); + // Prevent session fixation: invalidate old session and create new one + HttpSession oldSession = request.getSession(false); + if (oldSession != null) { + oldSession.invalidate(); + } + HttpSession session = request.getSession(true); + // Store user in Session AuthSession authSession = AuthSession.builder() .userId(user.getId()) diff --git a/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java b/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java index 3d3c09e..edb60a4 100644 --- a/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java +++ b/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java @@ -2,6 +2,8 @@ import dev.robothanzo.werewolf.database.documents.AuthSession; import dev.robothanzo.werewolf.database.documents.UserRole; +import dev.robothanzo.werewolf.service.DiscordService; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; @@ -9,8 +11,11 @@ import java.util.Optional; @Component +@RequiredArgsConstructor public class IdentityUtils { + private final DiscordService discordService; + public Optional getCurrentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); if (auth != null && auth.getPrincipal() instanceof AuthSession) { @@ -21,7 +26,14 @@ public Optional getCurrentUser() { public boolean hasAccess(long guildId) { return getCurrentUser() - .map(user -> String.valueOf(guildId).equals(user.getGuildId())) + .map(user -> { + // First check if the guild ID matches what's stored in the session + if (!String.valueOf(guildId).equals(user.getGuildId())) { + return false; + } + // Then verify the user is still actually a member of the guild + return discordService.getMember(guildId, user.getUserId()) != null; + }) .orElse(false); } From a53608e65f947bdd9fa5d2a0b9d6c63ecfed26e8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:33:24 +0000 Subject: [PATCH 3/5] Add input validation and error handling for parsing operations Co-authored-by: RobotHanzo <36107150+RobotHanzo@users.noreply.github.com> --- .../werewolf/WerewolfApplication.java | 5 ++- .../werewolf/listeners/ButtonListener.java | 38 +++++++++++-------- .../service/impl/GameSessionServiceImpl.java | 11 ++++-- 3 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/main/java/dev/robothanzo/werewolf/WerewolfApplication.java b/src/main/java/dev/robothanzo/werewolf/WerewolfApplication.java index ac99992..4e200e8 100644 --- a/src/main/java/dev/robothanzo/werewolf/WerewolfApplication.java +++ b/src/main/java/dev/robothanzo/werewolf/WerewolfApplication.java @@ -88,7 +88,10 @@ public static void extractSoundFiles() { new File(WerewolfApplication.class.getProtectionDomain().getCodeSource().getLocation().toURI())); File soundFolder = new File("sounds"); if (!soundFolder.exists()) { - soundFolder.mkdir(); + if (!soundFolder.mkdir()) { + log.error("Failed to create sounds directory"); + return; + } } // Logic to clean and extract (simplified from original to avoid full deletion // risk if not intent) diff --git a/src/main/java/dev/robothanzo/werewolf/listeners/ButtonListener.java b/src/main/java/dev/robothanzo/werewolf/listeners/ButtonListener.java index 23b9dfc..ec09c08 100644 --- a/src/main/java/dev/robothanzo/werewolf/listeners/ButtonListener.java +++ b/src/main/java/dev/robothanzo/werewolf/listeners/ButtonListener.java @@ -85,14 +85,18 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { event.getHook().editOriginal(":x: 你曾經參選過或正在參選,不得投票").queue(); return; } - Candidate electedCandidate = candidates - .get(Integer.parseInt(customId.replaceAll("votePolice", ""))); - if (electedCandidate != null) { - handleVote(event, candidates, electedCandidate); - // Broadcast update immediately - WerewolfApplication.gameSessionService.broadcastSessionUpdate(session); - } else { - event.getHook().editOriginal(":x: 找不到候選人").queue(); + try { + int candidateId = Integer.parseInt(customId.replace("votePolice", "")); + Candidate electedCandidate = candidates.get(candidateId); + if (electedCandidate != null) { + handleVote(event, candidates, electedCandidate); + // Broadcast update immediately + WerewolfApplication.gameSessionService.broadcastSessionUpdate(session); + } else { + event.getHook().editOriginal(":x: 找不到候選人").queue(); + } + } catch (NumberFormatException e) { + event.getHook().editOriginal(":x: 無效的投票選項").queue(); } } else { event.getHook().editOriginal(":x: 投票已過期").queue(); @@ -106,13 +110,17 @@ public void onButtonInteraction(@NotNull ButtonInteractionEvent event) { event.getHook().editOriginal(":x: 你正在和別人進行放逐辯論,不得投票").queue(); return; } - Map candidates = Poll.expelCandidates - .get(Objects.requireNonNull(event.getGuild()).getIdLong()); - Candidate electedCandidate = candidates - .get(Integer.parseInt(customId.replaceAll("voteExpel", ""))); - handleVote(event, candidates, electedCandidate); - // Broadcast update immediately for expel (user requested realtime voting) - WerewolfApplication.gameSessionService.broadcastSessionUpdate(session); + try { + Map candidates = Poll.expelCandidates + .get(Objects.requireNonNull(event.getGuild()).getIdLong()); + int candidateId = Integer.parseInt(customId.replace("voteExpel", "")); + Candidate electedCandidate = candidates.get(candidateId); + handleVote(event, candidates, electedCandidate); + // Broadcast update immediately for expel (user requested realtime voting) + WerewolfApplication.gameSessionService.broadcastSessionUpdate(session); + } catch (NumberFormatException e) { + event.getHook().editOriginal(":x: 無效的投票選項").queue(); + } } else { event.getHook().editOriginal(":x: 投票已過期").queue(); } diff --git a/src/main/java/dev/robothanzo/werewolf/service/impl/GameSessionServiceImpl.java b/src/main/java/dev/robothanzo/werewolf/service/impl/GameSessionServiceImpl.java index d2c211f..cbd8141 100644 --- a/src/main/java/dev/robothanzo/werewolf/service/impl/GameSessionServiceImpl.java +++ b/src/main/java/dev/robothanzo/werewolf/service/impl/GameSessionServiceImpl.java @@ -247,9 +247,14 @@ public List> playersToJSON(Session session) { } players.sort((a, b) -> { - int idA = Integer.parseInt((String) a.get("id")); - int idB = Integer.parseInt((String) b.get("id")); - return Integer.compare(idA, idB); + try { + int idA = Integer.parseInt((String) a.get("id")); + int idB = Integer.parseInt((String) b.get("id")); + return Integer.compare(idA, idB); + } catch (NumberFormatException e) { + // If parsing fails, treat as equal or use string comparison as fallback + return 0; + } }); return players; From 9cb8dc7d6e025b3fcc232f20382f3c4122b8b3a8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:37:38 +0000 Subject: [PATCH 4/5] Improve code clarity and add caching to reduce Discord API calls Co-authored-by: RobotHanzo <36107150+RobotHanzo@users.noreply.github.com> --- gradlew | 0 .../werewolf/config/SessionConfig.java | 7 +++- .../werewolf/utils/IdentityUtils.java | 40 ++++++++++++++++++- 3 files changed, 44 insertions(+), 3 deletions(-) mode change 100644 => 100755 gradlew diff --git a/gradlew b/gradlew old mode 100644 new mode 100755 diff --git a/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java b/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java index 489ff7a..806aeb9 100644 --- a/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java +++ b/src/main/java/dev/robothanzo/werewolf/config/SessionConfig.java @@ -15,7 +15,12 @@ public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setSameSite("Lax"); serializer.setUseHttpOnlyCookie(true); - serializer.setUseSecureCookie(!"http://localhost:5173".equals(System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173"))); + + // Use secure cookies for HTTPS environments (production) + String dashboardUrl = System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173"); + boolean isSecureEnvironment = dashboardUrl.startsWith("https://"); + serializer.setUseSecureCookie(isSecureEnvironment); + return serializer; } } diff --git a/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java b/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java index edb60a4..10c8f1a 100644 --- a/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java +++ b/src/main/java/dev/robothanzo/werewolf/utils/IdentityUtils.java @@ -8,13 +8,33 @@ import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import java.util.Map; import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; @Component @RequiredArgsConstructor public class IdentityUtils { private final DiscordService discordService; + + // Simple cache to reduce Discord API calls - entries expire after they're created + private static class MembershipCacheEntry { + final boolean isMember; + final long timestamp; + + MembershipCacheEntry(boolean isMember) { + this.isMember = isMember; + this.timestamp = System.currentTimeMillis(); + } + + boolean isExpired() { + // Cache for 5 minutes + return System.currentTimeMillis() - timestamp > 300_000; + } + } + + private final Map membershipCache = new ConcurrentHashMap<>(); public Optional getCurrentUser() { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); @@ -31,8 +51,24 @@ public boolean hasAccess(long guildId) { if (!String.valueOf(guildId).equals(user.getGuildId())) { return false; } - // Then verify the user is still actually a member of the guild - return discordService.getMember(guildId, user.getUserId()) != null; + + // Check cache first + String cacheKey = guildId + ":" + user.getUserId(); + MembershipCacheEntry cached = membershipCache.get(cacheKey); + if (cached != null && !cached.isExpired()) { + return cached.isMember; + } + + // Cache miss or expired - verify the user is still actually a member of the guild + boolean isMember = discordService.getMember(guildId, user.getUserId()) != null; + membershipCache.put(cacheKey, new MembershipCacheEntry(isMember)); + + // Clean up expired entries periodically (simple approach) + if (membershipCache.size() > 1000) { + membershipCache.entrySet().removeIf(e -> e.getValue().isExpired()); + } + + return isMember; }) .orElse(false); } From 31130ec50e9fa30ac909238d256fb569b0b85a58 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 2 Feb 2026 09:38:52 +0000 Subject: [PATCH 5/5] Fix potential open redirect vulnerability in OAuth callback Co-authored-by: RobotHanzo <36107150+RobotHanzo@users.noreply.github.com> --- .../werewolf/controller/AuthController.java | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java b/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java index 20d8aef..02eed05 100644 --- a/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java +++ b/src/main/java/dev/robothanzo/werewolf/controller/AuthController.java @@ -68,7 +68,7 @@ public void callback(@RequestParam String code, @RequestParam String state, if (!"no_guild".equals(state)) { try { - // Validate it's a number + // Validate it's a number - prevents open redirect attacks long gid = Long.parseLong(state); authSession.setGuildId(state); // Store as String @@ -80,14 +80,22 @@ public void callback(@RequestParam String code, @RequestParam String state, } else { authSession.setRole(UserRole.SPECTATOR); } + + session.setAttribute("user", authSession); + response.sendRedirect( + System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173") + "/server/" + state); + } catch (NumberFormatException e) { + // Invalid guild ID format - redirect to server selection instead + log.warn("Invalid guild ID in OAuth state: {}", state); + authSession.setRole(UserRole.PENDING); + session.setAttribute("user", authSession); + response.sendRedirect(System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173") + "/"); } catch (Exception e) { log.warn("Failed to set initial guild info: {}", state, e); authSession.setRole(UserRole.PENDING); + session.setAttribute("user", authSession); + response.sendRedirect(System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173") + "/"); } - - session.setAttribute("user", authSession); - response.sendRedirect( - System.getenv().getOrDefault("DASHBOARD_URL", "http://localhost:5173") + "/server/" + state); } else { authSession.setRole(UserRole.PENDING); session.setAttribute("user", authSession);