Fix CSRF bugs

This commit is contained in:
2026-01-05 17:45:54 +01:00
parent 0ebfc390c5
commit 929a0f2217
4 changed files with 18 additions and 4 deletions

View File

@@ -18,6 +18,7 @@ import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.authentication.HttpStatusEntryPoint; import org.springframework.security.web.authentication.HttpStatusEntryPoint;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.csrf.CookieCsrfTokenRepository; import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
import org.springframework.security.web.csrf.CsrfToken; import org.springframework.security.web.csrf.CsrfToken;
import org.springframework.security.web.csrf.CsrfException; import org.springframework.security.web.csrf.CsrfException;
import org.springframework.security.web.csrf.CsrfFilter; import org.springframework.security.web.csrf.CsrfFilter;
@@ -41,6 +42,9 @@ public class SecurityConfig {
HttpSecurity http, HttpSecurity http,
OAuth2AuthorizedClientRepository authorizedClientRepository OAuth2AuthorizedClientRepository authorizedClientRepository
) throws Exception { ) throws Exception {
CsrfTokenRequestAttributeHandler csrfRequestHandler = new CsrfTokenRequestAttributeHandler();
csrfRequestHandler.setCsrfRequestAttributeName("_csrf");
http http
.authorizeHttpRequests((auth) -> .authorizeHttpRequests((auth) ->
auth auth
@@ -89,6 +93,7 @@ public class SecurityConfig {
.csrf((csrf) -> .csrf((csrf) ->
csrf csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(csrfRequestHandler)
.ignoringRequestMatchers("/ws/**") .ignoringRequestMatchers("/ws/**")
) )
.addFilterAfter(csrfTokenCookieFilter(), CsrfFilter.class); .addFilterAfter(csrfTokenCookieFilter(), CsrfFilter.class);
@@ -140,7 +145,10 @@ public class SecurityConfig {
HttpServletResponse response, HttpServletResponse response,
FilterChain filterChain FilterChain filterChain
) throws java.io.IOException, jakarta.servlet.ServletException { ) throws java.io.IOException, jakarta.servlet.ServletException {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); CsrfToken csrfToken = (CsrfToken) request.getAttribute("_csrf");
if (csrfToken == null) {
csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
}
if (csrfToken != null) { if (csrfToken != null) {
String token = csrfToken.getToken(); String token = csrfToken.getToken();
Cookie existingCookie = WebUtils.getCookie(request, "XSRF-TOKEN"); Cookie existingCookie = WebUtils.getCookie(request, "XSRF-TOKEN");

View File

@@ -438,6 +438,7 @@ function connect() {
}, },
(error) => { (error) => {
console.warn("WebSocket connection issue", error); console.warn("WebSocket connection issue", error);
fetchAssets();
setTimeout( setTimeout(
() => showToast("Live updates connection interrupted. Retrying may be necessary.", "warning"), () => showToast("Live updates connection interrupted. Retrying may be necessary.", "warning"),
1000, 1000,
@@ -2086,7 +2087,7 @@ function uploadAsset(file = null) {
return; return;
} }
if (selectedFile.size > UPLOAD_LIMIT_BYTES) { if (selectedFile.size > UPLOAD_LIMIT_BYTES) {
showToast(`File is too large. Maximum upload size is ${UPLOAD_MAX_BYTES / 1024 / 1024} MB.`, "error"); showToast(`File is too large. Maximum upload size is ${UPLOAD_LIMIT_BYTES / 1024 / 1024} MB.`, "error");
return; return;
} }

View File

@@ -352,7 +352,7 @@
const broadcaster = /*[[${broadcaster}]]*/ ''; const broadcaster = /*[[${broadcaster}]]*/ '';
const username = /*[[${username}]]*/ ''; const username = /*[[${username}]]*/ '';
const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0; const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0;
const SETTINGS = /*[[${settingsJson}]]*/; const SETTINGS = JSON.parse(/*[[${settingsJson}]]*/);
</script> </script>
<script src="/js/cookie-consent.js"></script> <script src="/js/cookie-consent.js"></script>
<script src="/js/toast.js"></script> <script src="/js/toast.js"></script>

View File

@@ -1,6 +1,7 @@
package dev.kruhlmann.imgfloat; package dev.kruhlmann.imgfloat;
import static org.hamcrest.Matchers.hasSize; import static org.hamcrest.Matchers.hasSize;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.csrf;
import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login; import static org.springframework.security.test.web.servlet.request.SecurityMockMvcRequestPostProcessors.oauth2Login;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
@@ -48,6 +49,7 @@ class ChannelApiIntegrationTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"helper\"}") .content("{\"username\":\"helper\"}")
.with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster)))
.with(csrf())
) )
.andExpect(status().isOk()); .andExpect(status().isOk());
@@ -70,6 +72,7 @@ class ChannelApiIntegrationTest {
multipart("/api/channels/{broadcaster}/assets", broadcaster) multipart("/api/channels/{broadcaster}/assets", broadcaster)
.file(file) .file(file)
.with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster)))
.with(csrf())
) )
.andExpect(status().isOk()) .andExpect(status().isOk())
.andReturn() .andReturn()
@@ -96,6 +99,7 @@ class ChannelApiIntegrationTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(visibilityRequest)) .content(objectMapper.writeValueAsString(visibilityRequest))
.with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster)))
.with(csrf())
) )
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(jsonPath("$.hidden").value(false)); .andExpect(jsonPath("$.hidden").value(false));
@@ -113,7 +117,7 @@ class ChannelApiIntegrationTest {
.perform( .perform(
delete("/api/channels/{broadcaster}/assets/{id}", broadcaster, assetId).with( delete("/api/channels/{broadcaster}/assets/{id}", broadcaster, assetId).with(
oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster)) oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))
) ).with(csrf())
) )
.andExpect(status().isOk()); .andExpect(status().isOk());
} }
@@ -126,6 +130,7 @@ class ChannelApiIntegrationTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content("{\"username\":\"helper\"}") .content("{\"username\":\"helper\"}")
.with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", "intruder"))) .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", "intruder")))
.with(csrf())
) )
.andExpect(status().isForbidden()); .andExpect(status().isForbidden());
} }