From 065aa69323e6bfcf4d3496d3dd96332817ec0d35 Mon Sep 17 00:00:00 2001 From: Isaries Date: Wed, 9 Sep 2026 16:06:54 +0800 Subject: [PATCH] fix(security): complete the default-deny whitelist for anonymous endpoints The Spring Boot 3 upgrade (#311) added anyRequest().authenticated() but the permitAll whitelist was incomplete. This re-audits the security configuration against every unguarded frontend route and fills the gaps. Static asset paths served by WebConfig resource handlers (portal scripts, themes, translations, the VLE engine, project icons) were not excluded from the security filter chain, so the login page could not load its own JS, CSS or translations for anonymous visitors. Password recovery for teachers was actively blocked: /api/teacher/forgot/** matched the /api/teacher/** role rule (requires TEACHER) before reaching the terminal rule, making recovery impossible without the role the user is trying to recover. Student forgot paths fell through to authenticated. Both are now permitted before the role rules. Remaining anonymous endpoints are now explicitly permitted: the contact form, news, announcements (called on every page load by the root component), project info (used by the contact and library pages), Google registration checks, project preview, survey entry, and framework error dispatch paths. Remove @EnableWebSecurity(debug = true) which logs request headers and session cookies on every request. Gate DebugController behind @Profile(dev) and remove its permitAll rule so it is not registered or reachable in production. Remove the duplicate /api/user/info permitAll. --- .../web/controllers/DebugController.java | 2 + .../portal/spring/impl/WebSecurityConfig.java | 58 ++++++++++++++----- 2 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/DebugController.java b/src/main/java/org/wise/portal/presentation/web/controllers/DebugController.java index 36502c186..25041e22f 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/DebugController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/DebugController.java @@ -4,12 +4,14 @@ import java.util.Map; import java.util.stream.Collectors; +import org.springframework.context.annotation.Profile; import org.springframework.security.core.Authentication; import org.springframework.security.core.GrantedAuthority; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +@Profile("dev") @RestController @RequestMapping("/api/debug") public class DebugController { diff --git a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java index 9fc320ff2..d2dd890c1 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -70,7 +70,7 @@ import org.wise.portal.service.authentication.UserDetailsService; @Configuration -@EnableWebSecurity(debug = true) +@EnableWebSecurity @Order(SecurityProperties.BASIC_AUTH_ORDER - 10) public class WebSecurityConfig { @@ -96,38 +96,70 @@ public SecurityFilterChain filterChain(HttpSecurity http, SecurityContextHolderAwareRequestFilter.class) .authorizeHttpRequests( auth -> auth + // Static assets served by WebConfig resource handlers: without this the + // login page cannot load its own scripts, styles or translations. + .requestMatchers("/pages/resources/**", "/portal/javascript/**", + "/portal/themes/**", "/portal/translate/**", "/vle/**", + "/projectIcons/**") + .permitAll() .requestMatchers("/admin/account/**", "/admin/portal/**", "/admin/news/**", "/admin/mergeProjectMetadata", "/admin/project/updatesharedprojects", "/admin/run/replacebase64withpng.html", "/api/admin/**") .hasRole("ADMINISTRATOR") - .requestMatchers("/api/project/library", "/api/project/community", "/curriculum/**", - "/api/config/preview/**", "/api/user/info").permitAll() + .requestMatchers("/api/project/library", "/api/project/community", + "/curriculum/**", "/api/config/preview/**", "/api/user/info") + .permitAll() .requestMatchers(new AntPathRequestMatcher("/api/login/impersonate")) .hasAnyRole("ADMINISTRATOR", "RESEARCHER") .requestMatchers(new AntPathRequestMatcher("/admin/**")) .hasAnyRole("ADMINISTRATOR", "RESEARCHER") - .requestMatchers(new AntPathRequestMatcher("/author/**")).hasAnyRole("TEACHER") + .requestMatchers(new AntPathRequestMatcher("/author/**")) + .hasAnyRole("TEACHER") .requestMatchers(new AntPathRequestMatcher("/project/notifyAuthor*/**")) .hasAnyRole("TEACHER") .requestMatchers(new AntPathRequestMatcher("/student/account/info")) - .hasAnyRole("TEACHER").requestMatchers(new AntPathRequestMatcher("/student/**")) - .hasAnyRole("STUDENT").requestMatchers(new AntPathRequestMatcher("/studentStatus")) + .hasAnyRole("TEACHER") + .requestMatchers(new AntPathRequestMatcher("/student/**")) + .hasAnyRole("STUDENT") + .requestMatchers(new AntPathRequestMatcher("/studentStatus")) .hasAnyRole("TEACHER", "STUDENT") .requestMatchers(new AntPathRequestMatcher("/oauth2/**")).permitAll() .requestMatchers(new AntPathRequestMatcher("/login/oauth2/**")).permitAll() + // Password recovery must precede the /api/teacher/** role rule: + // without this a teacher who forgot their password needs the TEACHER + // role to start recovery. + .requestMatchers(new AntPathRequestMatcher("/api/student/forgot/**")) + .permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/teacher/forgot/**")) + .permitAll() .requestMatchers(new AntPathRequestMatcher("/api/teacher/register")).permitAll() .requestMatchers(new AntPathRequestMatcher("/api/student/register")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/student/register/questions")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/student/register/questions")) + .permitAll() .requestMatchers(new AntPathRequestMatcher("/api/*/register/**")).permitAll() - .requestMatchers(new AntPathRequestMatcher("/api/teacher/**")).hasAnyRole("TEACHER") + .requestMatchers(new AntPathRequestMatcher("/api/teacher/**")) + .hasAnyRole("TEACHER") .requestMatchers(new AntPathRequestMatcher("/sso/discourse")) .hasAnyRole("TEACHER", "STUDENT") - .requestMatchers(new AntPathRequestMatcher("/api/user/tags")).hasAnyRole("TEACHER") + .requestMatchers(new AntPathRequestMatcher("/api/user/tags")) + .hasAnyRole("TEACHER") .requestMatchers(new AntPathRequestMatcher("/api/user/tag/**")) - .hasAnyRole("TEACHER").requestMatchers(new AntPathRequestMatcher("/api/debug/**")) - .permitAll().requestMatchers(new AntPathRequestMatcher("/api/user/info")) - .permitAll().requestMatchers(new AntPathRequestMatcher("/api/user/config")) - .permitAll().requestMatchers(new AntPathRequestMatcher("/login")).permitAll() + .hasAnyRole("TEACHER") + .requestMatchers(new AntPathRequestMatcher("/api/user/config")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/contact")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/news/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/announcement")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/project/info/*")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/google-user/check-user-exists")) + .permitAll() + .requestMatchers(new AntPathRequestMatcher("/api/google-user/check-user-matches")) + .permitAll() + .requestMatchers(new AntPathRequestMatcher("/previewproject.html")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/run-survey/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/error")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/errors/**")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/favicon.ico")).permitAll() + .requestMatchers(new AntPathRequestMatcher("/login")).permitAll() .requestMatchers(new AntPathRequestMatcher("/")).permitAll() .anyRequest().authenticated()) .formLogin(form -> form.loginPage("/login").permitAll())