mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 00:14:56 +00:00
fix: secure demo file deletion
This commit is contained in:
@@ -408,8 +408,9 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
||||
# 启用后删除文件需要输入验证码,防止误删
|
||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||
|
||||
# 删除文件密码,默认为123456
|
||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
||||
# 删除文件密码,默认为false(禁用删除接口)
|
||||
# 如需启用删除功能,请通过环境变量或外部配置设置独立的强密码
|
||||
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||
|
||||
# 是否删除转换后的源文件,默认为true(删除)
|
||||
# 启用可节约磁盘空间,但会丢失原始文件
|
||||
|
||||
@@ -405,8 +405,8 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
||||
# 启用后删除文件需要输入验证码,防止误删
|
||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||
|
||||
# 删除文件密码,默认为123456
|
||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
||||
# 删除文件密码,默认为false(禁用删除接口)
|
||||
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||
|
||||
# 是否删除转换后的源文件,默认为true(删除)
|
||||
# 启用可节约磁盘空间,但会丢失原始文件
|
||||
|
||||
@@ -31,7 +31,7 @@ public class ConfigConstants {
|
||||
// ==================================================
|
||||
public static final String DEFAULT_VALUE = "default";
|
||||
public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456";
|
||||
public static final String DEFAULT_PASSWORD = "123456";
|
||||
public static final String DEFAULT_PASSWORD = "false";
|
||||
public static final String DEFAULT_SIZE = "500MB";
|
||||
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
|
||||
public static final String DEFAULT_IS_JAVASCRIPT = "false";
|
||||
@@ -664,7 +664,7 @@ public class ConfigConstants {
|
||||
public void setSize(String size) { setSizeValue(size); }
|
||||
public static void setSizeValue(String size) { ConfigConstants.size = size; }
|
||||
|
||||
@Value("${delete.password:123456}")
|
||||
@Value("${delete.password:false}")
|
||||
public void setPassword(String password) { setPasswordValue(password); }
|
||||
public static void setPasswordValue(String password) { ConfigConstants.password = password; }
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import cn.keking.utils.WebUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
import org.springframework.util.StreamUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
@@ -33,6 +34,8 @@ import java.nio.file.InvalidPathException;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.BasicFileAttributes;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.MessageDigest;
|
||||
import java.util.*;
|
||||
|
||||
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
|
||||
@@ -218,7 +221,7 @@ public class FileController {
|
||||
}
|
||||
}
|
||||
|
||||
@GetMapping("/deleteFile")
|
||||
@PostMapping("/deleteFile")
|
||||
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
|
||||
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
||||
if (checkResult.isFailure()) {
|
||||
@@ -775,11 +778,22 @@ public class FileController {
|
||||
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
||||
}
|
||||
|
||||
String expectedPassword = ConfigConstants.getDeleteCaptcha() ?
|
||||
boolean captchaEnabled = ConfigConstants.getDeleteCaptcha();
|
||||
String expectedPassword = captchaEnabled ?
|
||||
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
||||
ConfigConstants.getPassword();
|
||||
|
||||
if (!password.equalsIgnoreCase(expectedPassword)) {
|
||||
if (!captchaEnabled && (!StringUtils.hasText(expectedPassword)
|
||||
|| "false".equalsIgnoreCase(expectedPassword))) {
|
||||
return ReturnResponse.failure("文件删除接口已禁用,请先配置 delete.password");
|
||||
}
|
||||
|
||||
if (!StringUtils.hasText(expectedPassword)) {
|
||||
return ReturnResponse.failure("验证码已失效,请刷新后重试!");
|
||||
}
|
||||
|
||||
if (!MessageDigest.isEqual(password.getBytes(StandardCharsets.UTF_8),
|
||||
expectedPassword.getBytes(StandardCharsets.UTF_8))) {
|
||||
logger.error("删除文件【{}】失败,密码错误!", fileName);
|
||||
return ReturnResponse.failure("删除文件失败,密码错误!");
|
||||
}
|
||||
|
||||
@@ -369,8 +369,8 @@
|
||||
$("#deleteCaptchaConfirmBtn").click(function() {
|
||||
var fileName = $("#deleteCaptchaFileName").val();
|
||||
var deleteCaptchaText = $("#deleteCaptchaText").val();
|
||||
$.get('${baseUrl}deleteFile?fileName=' + fileName +'&password=' + deleteCaptchaText, function(data){
|
||||
if ("删除文件失败,密码错误!" === data.msg) {
|
||||
$.post('${baseUrl}deleteFile', {fileName: fileName, password: deleteCaptchaText}, function(data){
|
||||
if (!data.success) {
|
||||
alert(data.msg);
|
||||
} else {
|
||||
$('#table').bootstrapTable("refresh", {});
|
||||
@@ -392,11 +392,16 @@
|
||||
function deleteFile(fileName, isFolder) {
|
||||
var message = isFolder ? '你确定要删除这个文件夹吗?(包含所有子文件)' : '你确定要删除这个文件吗?';
|
||||
if (window.confirm(message)) {
|
||||
password = prompt("请输入默认密码:123456");
|
||||
var password = prompt("请输入文件删除密码");
|
||||
if (password === null) {
|
||||
return false;
|
||||
}
|
||||
$.ajax({
|
||||
url: '${baseUrl}deleteFile?fileName=' + fileName +'&password='+password,
|
||||
url: '${baseUrl}deleteFile',
|
||||
type: 'POST',
|
||||
data: {fileName: fileName, password: password},
|
||||
success: function (data) {
|
||||
if ("删除文件失败,密码错误!" === data.msg) {
|
||||
if (!data.success) {
|
||||
alert(data.msg);
|
||||
} else {
|
||||
$("#table").bootstrapTable("refresh", {});
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
package cn.keking.web.controller;
|
||||
|
||||
import cn.keking.config.ConfigConstants;
|
||||
import cn.keking.model.ReturnResponse;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.codec.binary.Base64;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import org.springframework.core.io.ClassPathResource;
|
||||
import org.springframework.mock.web.MockHttpServletRequest;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FileControllerDeleteSecurityTests {
|
||||
|
||||
@TempDir
|
||||
Path tempDir;
|
||||
|
||||
private String originalFileDir;
|
||||
private String originalPassword;
|
||||
private Boolean originalDeleteCaptcha;
|
||||
|
||||
@BeforeEach
|
||||
void configureDemoDirectory() throws IOException {
|
||||
originalFileDir = ConfigConstants.getFileDir();
|
||||
originalPassword = ConfigConstants.getPassword();
|
||||
originalDeleteCaptcha = ConfigConstants.getDeleteCaptcha();
|
||||
Files.createDirectory(tempDir.resolve("demo"));
|
||||
ConfigConstants.setFileDirValue(tempDir.toString());
|
||||
ConfigConstants.setDeleteCaptchaValue(false);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void restoreConfiguration() {
|
||||
ConfigConstants.setFileDirValue(originalFileDir);
|
||||
ConfigConstants.setPasswordValue(originalPassword);
|
||||
ConfigConstants.setDeleteCaptchaValue(originalDeleteCaptcha);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldDisableDeletionWhenNoPasswordIsConfigured() throws IOException {
|
||||
ConfigConstants.setPasswordValue("false");
|
||||
Path victim = Files.writeString(tempDir.resolve("demo/victim.txt"), "keep");
|
||||
FileController controller = new FileController();
|
||||
|
||||
ReturnResponse<Object> response = controller.deleteFile(
|
||||
new MockHttpServletRequest(), encodeFileName("victim.txt"), "false");
|
||||
|
||||
assertTrue(response.isFailure());
|
||||
assertTrue(Files.exists(victim));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldRequireAnExactCaseSensitivePassword() throws IOException {
|
||||
ConfigConstants.setPasswordValue("Strong-Delete-Password");
|
||||
Path victim = Files.writeString(tempDir.resolve("demo/victim.txt"), "delete me");
|
||||
FileController controller = new FileController();
|
||||
|
||||
ReturnResponse<Object> wrongCase = controller.deleteFile(
|
||||
new MockHttpServletRequest(), encodeFileName("victim.txt"), "strong-delete-password");
|
||||
assertTrue(wrongCase.isFailure());
|
||||
assertTrue(Files.exists(victim));
|
||||
|
||||
ReturnResponse<Object> correct = controller.deleteFile(
|
||||
new MockHttpServletRequest(), encodeFileName("victim.txt"), "Strong-Delete-Password");
|
||||
assertTrue(correct.isSuccess());
|
||||
assertFalse(Files.exists(victim));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeDeletionOnlyAsPost() throws NoSuchMethodException {
|
||||
Method method = FileController.class.getMethod(
|
||||
"deleteFile", HttpServletRequest.class, String.class, String.class);
|
||||
|
||||
assertNotNull(method.getAnnotation(PostMapping.class));
|
||||
assertNull(method.getAnnotation(GetMapping.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldKeepDeletionDisabledAndCredentialsOutOfUrlsByDefault() throws IOException {
|
||||
String properties = readResource("application.properties");
|
||||
String template = readResource("web/main/index.ftl");
|
||||
|
||||
assertTrue(properties.contains("delete.password = ${KK_DELETE_PASSWORD:false}"));
|
||||
assertTrue(template.contains("type: 'POST'"));
|
||||
assertTrue(template.contains("$.post('${baseUrl}deleteFile'"));
|
||||
assertFalse(template.contains("deleteFile?"));
|
||||
assertFalse(template.contains("默认密码:123456"));
|
||||
}
|
||||
|
||||
private String encodeFileName(String fileName) {
|
||||
String value = "file://localhost/" + fileName;
|
||||
return Base64.encodeBase64String(value.getBytes(StandardCharsets.UTF_8));
|
||||
}
|
||||
|
||||
private String readResource(String path) throws IOException {
|
||||
ClassPathResource resource = new ClassPathResource(path);
|
||||
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user