From 2d865312ee2516c5dbc2e77e55f43fce83990e5a Mon Sep 17 00:00:00 2001 From: Isaries Date: Mon, 6 Jul 2026 22:04:32 +0800 Subject: [PATCH 1/2] fix(security): deny unauthenticated access by default The authorization chain listed specific paths and ended without a final rule, so any request matching none of the rules was permitted. Most /api endpoints matched no rule and relied entirely on method-level annotations, so any endpoint missing an annotation was reachable without authentication. Add an explicit allow list for the endpoints that must remain public (sign-in and its OAuth callbacks, registration, account and password recovery, the contact form, the public home, news, library and preview content, and the bootstrap configuration) and require authentication for every other request, so a missing method annotation fails closed. --- .../portal/spring/impl/WebSecurityConfig.java | 34 +++++++++++++++---- 1 file changed, 27 insertions(+), 7 deletions(-) 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 b608d1272..0fd665e42 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,34 @@ 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() + .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") + // Endpoints that must stay reachable without authentication: sign-in and its + // OAuth callbacks, account registration, account and password recovery, the + // contact form, the public home, news, library and preview content, and the + // application bootstrap configuration. Every request that does not match a rule + // above falls through to the final rule and now requires authentication, rather + // than being permitted by default. + .antMatchers("/", "/login", "/login/**", "/errors/**", "/run-survey/**", + "/previewproject.html").permitAll() + .antMatchers("/api/j_acegi_security_check", "/api/google-login", "/api/microsoft-login", + "/api/logout").permitAll() + .antMatchers("/api/student/register", "/api/student/register/questions", + "/api/teacher/register", "/api/contact").permitAll() + .antMatchers("/api/student/forgot/**", "/api/teacher/forgot/**").permitAll() + .antMatchers("/api/user/config", "/api/user/info", "/api/user/languages", + "/api/announcement", "/api/news", "/api/project/library", "/api/project/community", + "/api/config/vle", "/api/config/preview/**", "/api/run/info", "/api/runInfo", + "/api/session/renew", "/api/google-user/check-user-exists").permitAll() + .anyRequest().authenticated(); http.formLogin().loginPage("/login").permitAll(); http.logout().addLogoutHandler(wiseLogoutHandler()) .logoutRequestMatcher(new AntPathRequestMatcher("/api/logout")); From 7b0728582c1ca0531da7f5f6160b80aee43388bd Mon Sep 17 00:00:00 2001 From: Isaries Date: Mon, 6 Jul 2026 23:02:29 +0800 Subject: [PATCH 2/2] fix(security): keep public static assets reachable under default-deny Denying unmatched requests by default also caught the static asset paths served by the resource handlers configured in WebConfig (portal scripts, themes and translations, the runtime engine, project files and project icons). Those are public content that the login page and the anonymous project preview need, so blocking them would break sign-in and preview for signed-out visitors. Permit those paths explicitly. Student uploaded files are intentionally left requiring authentication. --- .../org/wise/portal/spring/impl/WebSecurityConfig.java | 8 ++++++++ 1 file changed, 8 insertions(+) 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 0fd665e42..7e215778a 100644 --- a/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java +++ b/src/main/java/org/wise/portal/spring/impl/WebSecurityConfig.java @@ -86,6 +86,14 @@ protected void configure(HttpSecurity http) throws Exception { .addFilterAfter(microsoftOpenIdConnectFilter(), OAuth2ClientContextFilter.class) .addFilterAfter(authenticationProcessingFilter(), GoogleOpenIdConnectFilter.class) .authorizeRequests() + // Static assets served by the resource handlers configured in WebConfig are public + // content (portal scripts, themes and translations, the runtime engine, project + // files and project icons). They must stay reachable without signing in, now that + // unmatched requests are denied by default, otherwise the login and public preview + // pages would fail to load their assets. Student uploaded files are intentionally + // excluded so they continue to require authentication. + .antMatchers("/pages/resources/**", "/portal/javascript/**", "/portal/themes/**", + "/portal/translate/**", "/vle/**", "/curriculum/**", "/projectIcons/**").permitAll() .antMatchers("/api/login/impersonate").hasAnyRole("ADMINISTRATOR", "RESEARCHER") .antMatchers("/admin/**").hasAnyRole("ADMINISTRATOR", "RESEARCHER") .antMatchers("/author/**").hasAnyRole("TEACHER")