From 8b6b6f8315b5ad6ba42c32f14fd2efc8d40cdeec Mon Sep 17 00:00:00 2001 From: Isaries Date: Mon, 6 Jul 2026 21:41:03 +0800 Subject: [PATCH 1/3] fix(security): require administrator role for user and account management The admin surface was gated by a single URL rule allowing both administrators and researchers, so a researcher could reach the user, role, portal and news management endpoints and grant themselves the administrator role, disable accounts, or read other users' details. Restrict those endpoints to administrators at the URL layer and add method-level enforcement on the account management controllers so the restriction does not depend on the URL configuration alone. --- .../BatchCreateUserAccountsController.java | 2 ++ .../admin/EnableDisableUserController.java | 2 ++ .../admin/LookupUserController.java | 2 ++ .../admin/ManageUserRolesController.java | 2 ++ .../admin/ShowAllUsersController.java | 2 ++ ...ProjectSharedPermissionsAPIController.java | 2 ++ .../portal/spring/impl/WebSecurityConfig.java | 25 +++++++++++++------ 7 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/BatchCreateUserAccountsController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/BatchCreateUserAccountsController.java index 1426aa3307..7fb9918775 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/BatchCreateUserAccountsController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/BatchCreateUserAccountsController.java @@ -30,6 +30,7 @@ import java.util.Calendar; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -55,6 +56,7 @@ * * @author Hiroki Terashima */ +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/account/batchcreateuseraccounts.html") public class BatchCreateUserAccountsController { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/EnableDisableUserController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/EnableDisableUserController.java index e40c3c53e1..683e52ba28 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/EnableDisableUserController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/EnableDisableUserController.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; @@ -41,6 +42,7 @@ * Only accessed by a WISE admin user. * @author Hiroki Terashima */ +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/account/enabledisableuser") public class EnableDisableUserController { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/LookupUserController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/LookupUserController.java index 0f9b93adc5..009b7bc36b 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/LookupUserController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/LookupUserController.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletRequest; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.validation.BindingResult; @@ -50,6 +51,7 @@ * @author Patrick Lawler * @author Hiroki Terashima */ +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/account/lookupuser") public class LookupUserController { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/ManageUserRolesController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/ManageUserRolesController.java index 5462caab08..a669be6277 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/ManageUserRolesController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/ManageUserRolesController.java @@ -29,6 +29,7 @@ import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.security.core.GrantedAuthority; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; @@ -44,6 +45,7 @@ * * @author Hiroki Terashima */ +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/account/manageuserroles.html") public class ManageUserRolesController { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/ShowAllUsersController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/ShowAllUsersController.java index ea7aae0b5e..38cb480bf8 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/ShowAllUsersController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/ShowAllUsersController.java @@ -1,6 +1,7 @@ package org.wise.portal.presentation.web.controllers.admin; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; @@ -8,6 +9,7 @@ import org.springframework.web.bind.annotation.RequestParam; import org.wise.portal.service.authentication.UserDetailsService; +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/account/show-all-users") public class ShowAllUsersController { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateProjectSharedPermissionsAPIController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateProjectSharedPermissionsAPIController.java index 74a352317b..cbff00917d 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateProjectSharedPermissionsAPIController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateProjectSharedPermissionsAPIController.java @@ -1,6 +1,7 @@ package org.wise.portal.presentation.web.controllers.admin; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.security.acls.model.Permission; import org.springframework.web.bind.annotation.*; import org.wise.portal.dao.ObjectNotFoundException; @@ -13,6 +14,7 @@ import java.util.List; import java.util.Set; +@Secured("ROLE_ADMINISTRATOR") @RestController @RequestMapping("/api/admin/project/shared") public class UpdateProjectSharedPermissionsAPIController { 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 b608d1272e..0107e87e38 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -85,14 +85,25 @@ protected void configure(HttpSecurity http) throws Exception { .addFilterAfter(googleOpenIdConnectFilter(), OAuth2ClientContextFilter.class) .addFilterAfter(microsoftOpenIdConnectFilter(), OAuth2ClientContextFilter.class) .addFilterAfter(authenticationProcessingFilter(), GoogleOpenIdConnectFilter.class) - .authorizeRequests().antMatchers("/api/login/impersonate") - .hasAnyRole("ADMINISTRATOR", "RESEARCHER").antMatchers("/admin/**") - .hasAnyRole("ADMINISTRATOR", "RESEARCHER").antMatchers("/author/**").hasAnyRole("TEACHER") + .authorizeRequests() + // User, role, portal, news and destructive maintenance endpoints must be + // restricted to administrators. They were previously reachable by researchers + // through the broad "/admin/**" rule, which let a researcher grant themselves + // the administrator role or manage other users' accounts. These more specific + // rules are listed first so they take precedence over the broader ones below. + .antMatchers("/admin/account/**", "/admin/portal/**", "/admin/news/**", + "/admin/mergeProjectMetadata", "/admin/run/replacebase64withpng.html", "/api/admin/**") + .hasRole("ADMINISTRATOR") + .antMatchers("/api/login/impersonate").hasAnyRole("ADMINISTRATOR", "RESEARCHER") + .antMatchers("/admin/**").hasAnyRole("ADMINISTRATOR", "RESEARCHER") + .antMatchers("/author/**").hasAnyRole("TEACHER") .antMatchers("/project/notifyAuthor*/**").hasAnyRole("TEACHER") - .antMatchers("/student/account/info").hasAnyRole("TEACHER").antMatchers("/student/**") - .hasAnyRole("STUDENT").antMatchers("/studentStatus").hasAnyRole("TEACHER", "STUDENT") - .antMatchers("/teacher/**").hasAnyRole("TEACHER").antMatchers("/sso/discourse") - .hasAnyRole("TEACHER", "STUDENT").antMatchers("/").permitAll(); + .antMatchers("/student/account/info").hasAnyRole("TEACHER") + .antMatchers("/student/**").hasAnyRole("STUDENT") + .antMatchers("/studentStatus").hasAnyRole("TEACHER", "STUDENT") + .antMatchers("/teacher/**").hasAnyRole("TEACHER") + .antMatchers("/sso/discourse").hasAnyRole("TEACHER", "STUDENT") + .antMatchers("/").permitAll(); http.formLogin().loginPage("/login").permitAll(); http.logout().addLogoutHandler(wiseLogoutHandler()) .logoutRequestMatcher(new AntPathRequestMatcher("/api/logout")); From 6c825b1f389e6d082decb7b13ae23e9241b2ebae Mon Sep 17 00:00:00 2001 From: Isaries Date: Thu, 9 Jul 2026 22:47:24 +0800 Subject: [PATCH 2/3] fix(security): hide admin-only links in the admin index page The user management links in the admin index page pointed at endpoints that now require the administrator role, so a researcher would follow them into a 403. Wrap the whole user management section in instead, which also drops the nested check around the logged-in user lists. Require the administrator role for /admin/project/updatesharedprojects as well. The page only drives the /api/admin/project/shared endpoints, which administrators alone may call. Drop the WebSecurityConfig comments; the rules speak for themselves. --- .../admin/UpdateSharedProjectsController.java | 2 + .../portal/spring/impl/WebSecurityConfig.java | 8 +-- src/main/webapp/portal/admin/index.jsp | 62 +++++++++---------- 3 files changed, 35 insertions(+), 37 deletions(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateSharedProjectsController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateSharedProjectsController.java index 69070fab16..07e1c4dc88 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateSharedProjectsController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/UpdateSharedProjectsController.java @@ -1,11 +1,13 @@ package org.wise.portal.presentation.web.controllers.admin; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.servlet.ModelAndView; +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/project/updatesharedprojects") public class UpdateSharedProjectsController { 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 0107e87e38..744ad5fa28 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -86,13 +86,9 @@ protected void configure(HttpSecurity http) throws Exception { .addFilterAfter(microsoftOpenIdConnectFilter(), OAuth2ClientContextFilter.class) .addFilterAfter(authenticationProcessingFilter(), GoogleOpenIdConnectFilter.class) .authorizeRequests() - // User, role, portal, news and destructive maintenance endpoints must be - // restricted to administrators. They were previously reachable by researchers - // through the broad "/admin/**" rule, which let a researcher grant themselves - // the administrator role or manage other users' accounts. These more specific - // rules are listed first so they take precedence over the broader ones below. .antMatchers("/admin/account/**", "/admin/portal/**", "/admin/news/**", - "/admin/mergeProjectMetadata", "/admin/run/replacebase64withpng.html", "/api/admin/**") + "/admin/mergeProjectMetadata", "/admin/project/updatesharedprojects", + "/admin/run/replacebase64withpng.html", "/api/admin/**") .hasRole("ADMINISTRATOR") .antMatchers("/api/login/impersonate").hasAnyRole("ADMINISTRATOR", "RESEARCHER") .antMatchers("/admin/**").hasAnyRole("ADMINISTRATOR", "RESEARCHER") diff --git a/src/main/webapp/portal/admin/index.jsp b/src/main/webapp/portal/admin/index.jsp index a5113f67cc..7852fb6a91 100644 --- a/src/main/webapp/portal/admin/index.jsp +++ b/src/main/webapp/portal/admin/index.jsp @@ -58,12 +58,12 @@
-
- -
-
-
- + +
+ +
+
+
@@ -76,34 +76,34 @@ | - -
-
- - - | - - -
-
- - - | - - +
+
+ + + | + + +
+
+ + + | + + -
-
- - -
- +
- - + +
- -
+ +
+ + +
+
+
+
From c4f9e4b0f73e9949ff258e448323a3a541d2eb00 Mon Sep 17 00:00:00 2001 From: Isaries Date: Thu, 9 Jul 2026 23:25:35 +0800 Subject: [PATCH 3/3] fix(security): close trailing slash bypass on exact admin paths The admin-only rules for /admin/mergeProjectMetadata and /admin/run/replacebase64withpng.html were exact ant patterns. Spring MVC matches a trailing slash and routes /admin/mergeProjectMetadata/ to the controller, but the ant matcher does not, so the request fell through to the broader /admin/** rule and a researcher reached both endpoints. Match these paths the way Spring MVC matches them, and secure the two controllers directly so the rules are not the only guard. --- .../web/controllers/admin/AdminUtilsController.java | 2 ++ .../web/controllers/run/ReplaceBase64WithPNGController.java | 2 ++ .../java/org/wise/portal/spring/impl/WebSecurityConfig.java | 4 +++- 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/admin/AdminUtilsController.java b/src/main/java/org/wise/portal/presentation/web/controllers/admin/AdminUtilsController.java index 03518ec5e5..9338c4976d 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/admin/AdminUtilsController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/admin/AdminUtilsController.java @@ -24,6 +24,7 @@ package org.wise.portal.presentation.web.controllers.admin; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.RequestMapping; import org.wise.portal.domain.project.Project; @@ -42,6 +43,7 @@ * Admin utility functions like db migrations and batch scripts * @author Hiroki Terashima */ +@Secured("ROLE_ADMINISTRATOR") @Controller public class AdminUtilsController { diff --git a/src/main/java/org/wise/portal/presentation/web/controllers/run/ReplaceBase64WithPNGController.java b/src/main/java/org/wise/portal/presentation/web/controllers/run/ReplaceBase64WithPNGController.java index 1ac9d4a1a7..b7dde93a80 100644 --- a/src/main/java/org/wise/portal/presentation/web/controllers/run/ReplaceBase64WithPNGController.java +++ b/src/main/java/org/wise/portal/presentation/web/controllers/run/ReplaceBase64WithPNGController.java @@ -3,6 +3,7 @@ import org.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.core.env.Environment; +import org.springframework.security.access.annotation.Secured; import org.springframework.stereotype.Controller; import org.springframework.ui.ModelMap; import org.springframework.web.bind.annotation.GetMapping; @@ -32,6 +33,7 @@ * student data, it will replace the Base64 string with a reference to that png image in the * studentuploads folder. */ +@Secured("ROLE_ADMINISTRATOR") @Controller @RequestMapping("/admin/run/replacebase64withpng.html") public class ReplaceBase64WithPNGController { 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 744ad5fa28..d98f79e2e0 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -86,7 +86,9 @@ protected void configure(HttpSecurity http) throws Exception { .addFilterAfter(microsoftOpenIdConnectFilter(), OAuth2ClientContextFilter.class) .addFilterAfter(authenticationProcessingFilter(), GoogleOpenIdConnectFilter.class) .authorizeRequests() - .antMatchers("/admin/account/**", "/admin/portal/**", "/admin/news/**", + // Matched the way Spring MVC matches, so a trailing slash cannot slip an exact path + // past these rules and into the broader "/admin/**" rule below. + .mvcMatchers("/admin/account/**", "/admin/portal/**", "/admin/news/**", "/admin/mergeProjectMetadata", "/admin/project/updatesharedprojects", "/admin/run/replacebase64withpng.html", "/api/admin/**") .hasRole("ADMINISTRATOR")