Sanitize log input

This commit is contained in:
2026-01-05 18:20:12 +01:00
parent 68b23ee190
commit 7c0b3eaff1
8 changed files with 41 additions and 25 deletions

View File

@@ -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

View File

@@ -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(
exceptions
.defaultAuthenticationEntryPointFor(
new HttpStatusEntryPoint(HttpStatus.UNAUTHORIZED),
new AntPathRequestMatcher("/api/**")
).accessDeniedHandler(csrfAccessDeniedHandler())
)
.accessDeniedHandler(csrfAccessDeniedHandler())
)
.csrf((csrf) ->
csrf

View File

@@ -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");
});
}

View File

@@ -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);

View File

@@ -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 {}",

View File

@@ -129,11 +129,14 @@ public class ChannelDirectoryService {
public Optional<AssetView> 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);

View File

@@ -349,8 +349,8 @@
</div>
</div>
<script th:inline="javascript">
const broadcaster = /*[[${broadcaster}]]*/ '';
const username = /*[[${username}]]*/ '';
const broadcaster = /*[[${broadcaster}]]*/ "";
const username = /*[[${username}]]*/ "";
const UPLOAD_LIMIT_BYTES = /*[[${uploadLimitBytes}]]*/ 0;
const SETTINGS = JSON.parse(/*[[${settingsJson}]]*/);
</script>

View File

@@ -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());
}