diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java b/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java index 118ba8e..44a36b9 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/SchemaMigration.java @@ -103,6 +103,7 @@ public class SchemaMigration implements ApplicationRunner { return; } + // SECURITY: This is ok, because tableName and columnName are controlled internally and not from user input. try { jdbcTemplate.execute( "ALTER TABLE " + tableName + " ADD COLUMN " + columnName + " " + dataType + " DEFAULT " + defaultValue diff --git a/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java b/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java index d10e149..a1ea42d 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java +++ b/src/main/java/dev/kruhlmann/imgfloat/config/SecurityConfig.java @@ -1,5 +1,9 @@ package dev.kruhlmann.imgfloat.config; +import jakarta.servlet.FilterChain; +import jakarta.servlet.http.Cookie; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.annotation.Bean; @@ -16,19 +20,15 @@ import org.springframework.security.oauth2.client.web.OAuth2AuthorizedClientRepo import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.HttpStatusEntryPoint; -import org.springframework.security.web.util.matcher.AntPathRequestMatcher; 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.CsrfException; import org.springframework.security.web.csrf.CsrfFilter; +import org.springframework.security.web.csrf.CsrfToken; +import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler; +import org.springframework.security.web.util.matcher.AntPathRequestMatcher; +import org.springframework.web.client.RestTemplate; import org.springframework.web.filter.OncePerRequestFilter; import org.springframework.web.util.WebUtils; -import org.springframework.web.client.RestTemplate; -import jakarta.servlet.FilterChain; -import jakarta.servlet.http.Cookie; -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; @Configuration @EnableWebSecurity @@ -85,10 +85,12 @@ public class SecurityConfig { ) .logout((logout) -> logout.logoutSuccessUrl("/").permitAll()) .exceptionHandling((exceptions) -> - exceptions.defaultAuthenticationEntryPointFor( - new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), - new AntPathRequestMatcher("/api/**") - ).accessDeniedHandler(csrfAccessDeniedHandler()) + exceptions + .defaultAuthenticationEntryPointFor( + new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED), + new AntPathRequestMatcher("/api/**") + ) + .accessDeniedHandler(csrfAccessDeniedHandler()) ) .csrf((csrf) -> csrf diff --git a/src/main/java/dev/kruhlmann/imgfloat/controller/ChannelApiController.java b/src/main/java/dev/kruhlmann/imgfloat/controller/ChannelApiController.java index f7a7ad7..929eff8 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/controller/ChannelApiController.java +++ b/src/main/java/dev/kruhlmann/imgfloat/controller/ChannelApiController.java @@ -124,7 +124,7 @@ public class ChannelApiController { LOG.warn( "No authorized Twitch client found for {} while fetching admin suggestions for {}", sessionUsername, - broadcaster + broadcaster.replaceAll("[\n\r]", "_") ); return List.of(); } @@ -185,7 +185,7 @@ public class ChannelApiController { authorizationService.userMatchesSessionUsernameOrThrowHttpError(broadcaster, sessionUsername); LOG.info( "Updating canvas for {} by {}: {}x{}", - broadcaster, + broadcaster.replaceAll("[\n\r]", "_"), sessionUsername, request.getWidth(), request.getHeight() @@ -276,7 +276,7 @@ public class ChannelApiController { LOG.info( "Updating visibility for asset {} on {} by {} to hidden={} ", assetId, - broadcaster, + broadcaster.replaceAll("[\n\r]", "_"), sessionUsername, request.isHidden() ); @@ -284,7 +284,12 @@ public class ChannelApiController { .updateVisibility(broadcaster, assetId, request) .map(ResponseEntity::ok) .orElseThrow(() -> { - LOG.warn("Visibility update for missing asset {} on {} by {}", assetId, broadcaster, sessionUsername); + LOG.warn( + "Visibility update for missing asset {} on {} by {}", + assetId.replaceAll("[\n\r]", "_"), + broadcaster.replaceAll("[\n\r]", "_"), + sessionUsername + ); return new ResponseStatusException(NOT_FOUND, "Asset not found"); }); } diff --git a/src/main/java/dev/kruhlmann/imgfloat/controller/ViewController.java b/src/main/java/dev/kruhlmann/imgfloat/controller/ViewController.java index e97d05a..d7849aa 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/controller/ViewController.java +++ b/src/main/java/dev/kruhlmann/imgfloat/controller/ViewController.java @@ -95,7 +95,11 @@ public class ViewController { broadcaster, sessionUsername ); - LOG.info("Rendering admin console for {} (requested by {})", broadcaster, sessionUsername); + LOG.info( + "Rendering admin console for {} (requested by {})", + broadcaster.replaceAll("[\n\r]", "_"), + sessionUsername + ); Settings settings = settingsService.get(); model.addAttribute("broadcaster", broadcaster.toLowerCase()); model.addAttribute("username", sessionUsername); diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/AuthorizationService.java b/src/main/java/dev/kruhlmann/imgfloat/service/AuthorizationService.java index b6fe7e0..cc70830 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/AuthorizationService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/AuthorizationService.java @@ -51,6 +51,7 @@ public class AuthorizationService { String broadcaster, String sessionUsername ) { + broadcaster = broadcaster.replaceAll("[\n\r]", "_"); if (!userIsBroadcasterOrChannelAdminForBroadcaster(broadcaster, sessionUsername)) { LOG.warn( "Access denied for broadcaster/admin-only action by user {} on broadcaster {}", diff --git a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java index 01627ba..d1ea3ee 100644 --- a/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java +++ b/src/main/java/dev/kruhlmann/imgfloat/service/ChannelDirectoryService.java @@ -129,11 +129,14 @@ public class ChannelDirectoryService { public Optional createAsset(String broadcaster, MultipartFile file) throws IOException { long fileSize = file.getSize(); - long maxSize = uploadLimitBytes; - if (fileSize > maxSize) { + if (fileSize > uploadLimitBytes) { throw new ResponseStatusException( PAYLOAD_TOO_LARGE, - String.format("Uploaded file is too large (%d bytes). Maximum allowed is %d bytes.", fileSize, maxSize) + String.format( + "Uploaded file is too large (%d bytes). Maximum allowed is %d bytes.", + fileSize, + uploadLimitBytes + ) ); } Channel channel = getOrCreateChannel(broadcaster); diff --git a/src/main/resources/templates/admin.html b/src/main/resources/templates/admin.html index 2adf23f..3661e43 100644 --- a/src/main/resources/templates/admin.html +++ b/src/main/resources/templates/admin.html @@ -349,8 +349,8 @@ diff --git a/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java b/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java index 37fa7a5..632f239 100644 --- a/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java +++ b/src/test/java/dev/kruhlmann/imgfloat/ChannelApiIntegrationTest.java @@ -115,9 +115,9 @@ class ChannelApiIntegrationTest { mockMvc .perform( - delete("/api/channels/{broadcaster}/assets/{id}", broadcaster, assetId).with( - oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster)) - ).with(csrf()) + delete("/api/channels/{broadcaster}/assets/{id}", broadcaster, assetId) + .with(oauth2Login().attributes((attrs) -> attrs.put("preferred_username", broadcaster))) + .with(csrf()) ) .andExpect(status().isOk()); }