diff --git a/src/main/java/com/ujcms/cms/core/SecurityConfig.java b/src/main/java/com/ujcms/cms/core/SecurityConfig.java index 9abbdff8c389eb3fbea3fb6b610de8005e988595..2048e355ea43b98904812373225ffc0a92e0b348 100644 --- a/src/main/java/com/ujcms/cms/core/SecurityConfig.java +++ b/src/main/java/com/ujcms/cms/core/SecurityConfig.java @@ -42,6 +42,9 @@ import org.springframework.security.oauth2.server.resource.web.BearerTokenAuthen import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; +import org.springframework.security.web.csrf.CookieCsrfTokenRepository; +import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter; +import org.springframework.security.web.header.writers.XXssProtectionHeaderWriter; import com.google.common.collect.ImmutableMap; import com.google.common.collect.ImmutableSet; @@ -135,6 +138,28 @@ public class SecurityConfig { IpLoginAttemptService ipLoginAttemptService) throws Exception { // 无后缀的请求 http.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) + // 启用CSRF防护 + .csrf(csrf -> csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())) + // 完善安全头部配置 + .headers(headers -> headers + // 启用HSTS + .httpStrictTransportSecurity(hsts -> hsts + .includeSubDomains(true) + .maxAgeInSeconds(31536000) // 1 year + ) + // 启用内容安全策略 + .contentSecurityPolicy(csp -> csp + .policyDirectives("default-src 'self'; script-src 'self' 'unsafe-inline' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self';") + ) + // 启用X-Content-Type-Options + .contentTypeOptions(Customizer.withDefaults()) + // 启用X-Frame-Options + .frameOptions(frame -> frame.sameOrigin()) + // 启用X-XSS-Protection + .xssProtection(xss -> xss.headerValue(XXssProtectionHeaderWriter.HeaderValue.ENABLED)) + // 启用Referrer-Policy + .referrerPolicy(referrer -> referrer.policy(ReferrerPolicyHeaderWriter.ReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN)) + ) .rememberMe(rememberMe -> rememberMe.tokenRepository(persistentTokenRepository(dataSource))) .logout(logout -> logout.logoutSuccessUrl("/")) // 使用Exception中的message信息。AccessDeniedHandlerImpl不使用Exception中的message信息会丢失 diff --git a/src/main/java/com/ujcms/cms/core/web/backendapi/AbstractUploadController.java b/src/main/java/com/ujcms/cms/core/web/backendapi/AbstractUploadController.java index eb5ef980a903311113b344c72d3d3333d04606d2..4e9b63b942025772de94f0dc7c6907d00279ebd7 100644 --- a/src/main/java/com/ujcms/cms/core/web/backendapi/AbstractUploadController.java +++ b/src/main/java/com/ujcms/cms/core/web/backendapi/AbstractUploadController.java @@ -27,6 +27,7 @@ import jakarta.servlet.http.HttpServletRequest; import java.io.File; import java.io.IOException; import java.nio.file.Files; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -69,6 +70,12 @@ public abstract class AbstractUploadController { extension, props.getUploadsExtensionBlacklist())); } validateType(types, extension); + // 检查文件内容的MIME类型 + String contentType = multipart.getContentType(); + if (contentType != null && !isValidContentType(contentType, extension)) { + throw new Http400Exception(String.format("file content type not allowed: '%s' for extension '%s'", + contentType, extension)); + } FileHandler fileHandler = Contexts.getCurrentSite().getConfig().getUploadStorage().getFileHandler(pathResolver); File tempFile = Files.createTempFile(null, "." + extension).toFile(); @@ -234,6 +241,35 @@ public abstract class AbstractUploadController { Site site, Long userId, Map result) throws IOException, EncoderException; } + /** + * 验证文件的MIME类型是否与文件后缀匹配 + * + * @param contentType 文件的MIME类型 + * @param extension 文件后缀 + * @return 是否有效 + */ + protected boolean isValidContentType(String contentType, String extension) { + extension = StringUtils.lowerCase(extension); + // 图片类型 + if (Arrays.asList("jpg", "jpeg", "png", "gif", "bmp", "webp", "svg").contains(extension)) { + return contentType.startsWith("image/"); + } + // 视频类型 + if (Arrays.asList("mp4", "avi", "mov", "wmv", "flv", "mkv").contains(extension)) { + return contentType.startsWith("video/"); + } + // 音频类型 + if (Arrays.asList("mp3", "wav", "ogg", "aac", "flac").contains(extension)) { + return contentType.startsWith("audio/"); + } + // 文档类型 + if (Arrays.asList("doc", "docx", "xls", "xlsx", "ppt", "pptx", "pdf", "txt").contains(extension)) { + return contentType.startsWith("application/") || contentType.equals("text/plain"); + } + // 其他类型 + return true; + } + public static class CropParams { private String url; private int x; diff --git a/src/main/resources/com/ujcms/cms/core/mapper/ArticleMapper.xml b/src/main/resources/com/ujcms/cms/core/mapper/ArticleMapper.xml index 7208362a85e5f4390e63ad86da7599cf7d6159d1..de44f7c1c4cbd42e19030d4a297cd4d74e157244 100644 --- a/src/main/resources/com/ujcms/cms/core/mapper/ArticleMapper.xml +++ b/src/main/resources/com/ujcms/cms/core/mapper/ArticleMapper.xml @@ -183,7 +183,7 @@ update ujcms_article set modified_user_id_ = #{toUserId} - where modified_user_id_ = ${fromUserId} + where modified_user_id_ = #{fromUserId} update ujcms_article