mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 00:14:56 +00:00
Merge commit from fork
Fix insecure default file deletion
This commit is contained in:
@@ -408,8 +408,9 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
|||||||
# 启用后删除文件需要输入验证码,防止误删
|
# 启用后删除文件需要输入验证码,防止误删
|
||||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||||
|
|
||||||
# 删除文件密码,默认为123456
|
# 删除文件密码,默认为false(禁用删除接口)
|
||||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
# 如需启用删除功能,请通过环境变量或外部配置设置独立的强密码
|
||||||
|
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||||
|
|
||||||
# 是否删除转换后的源文件,默认为true(删除)
|
# 是否删除转换后的源文件,默认为true(删除)
|
||||||
# 启用可节约磁盘空间,但会丢失原始文件
|
# 启用可节约磁盘空间,但会丢失原始文件
|
||||||
|
|||||||
@@ -405,8 +405,8 @@ home.pagesize = ${DEFAULT_HOME_PAGSIZE:20}
|
|||||||
# 启用后删除文件需要输入验证码,防止误删
|
# 启用后删除文件需要输入验证码,防止误删
|
||||||
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
delete.captcha = ${KK_DELETE_CAPTCHA:false}
|
||||||
|
|
||||||
# 删除文件密码,默认为123456
|
# 删除文件密码,默认为false(禁用删除接口)
|
||||||
delete.password = ${KK_DELETE_PASSWORD:123456}
|
delete.password = ${KK_DELETE_PASSWORD:false}
|
||||||
|
|
||||||
# 是否删除转换后的源文件,默认为true(删除)
|
# 是否删除转换后的源文件,默认为true(删除)
|
||||||
# 启用可节约磁盘空间,但会丢失原始文件
|
# 启用可节约磁盘空间,但会丢失原始文件
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ public class ConfigConstants {
|
|||||||
// ==================================================
|
// ==================================================
|
||||||
public static final String DEFAULT_VALUE = "default";
|
public static final String DEFAULT_VALUE = "default";
|
||||||
public static final String DEFAULT_SHOW_AES_KEY = "1234567890123456";
|
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_SIZE = "500MB";
|
||||||
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
|
public static final String DEFAULT_ENABLE_REFRECSHSCHEDULE = "5";
|
||||||
public static final String DEFAULT_IS_JAVASCRIPT = "false";
|
public static final String DEFAULT_IS_JAVASCRIPT = "false";
|
||||||
@@ -664,7 +664,7 @@ public class ConfigConstants {
|
|||||||
public void setSize(String size) { setSizeValue(size); }
|
public void setSize(String size) { setSizeValue(size); }
|
||||||
public static void setSizeValue(String size) { ConfigConstants.size = 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 void setPassword(String password) { setPasswordValue(password); }
|
||||||
public static void setPasswordValue(String password) { ConfigConstants.password = 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.Logger;
|
||||||
import org.slf4j.LoggerFactory;
|
import org.slf4j.LoggerFactory;
|
||||||
import org.springframework.util.ObjectUtils;
|
import org.springframework.util.ObjectUtils;
|
||||||
|
import org.springframework.util.StringUtils;
|
||||||
import org.springframework.util.StreamUtils;
|
import org.springframework.util.StreamUtils;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.PostMapping;
|
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.Path;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
import java.nio.file.attribute.BasicFileAttributes;
|
import java.nio.file.attribute.BasicFileAttributes;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.security.MessageDigest;
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
|
||||||
import static cn.keking.utils.CaptchaUtil.CAPTCHA_CODE;
|
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) {
|
public ReturnResponse<Object> deleteFile(HttpServletRequest request, String fileName, String password) {
|
||||||
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
ReturnResponse<Object> checkResult = this.deleteFileCheck(request, fileName, password);
|
||||||
if (checkResult.isFailure()) {
|
if (checkResult.isFailure()) {
|
||||||
@@ -775,11 +778,22 @@ public class FileController {
|
|||||||
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
return ReturnResponse.failure("密码 or 验证码为空,删除失败!");
|
||||||
}
|
}
|
||||||
|
|
||||||
String expectedPassword = ConfigConstants.getDeleteCaptcha() ?
|
boolean captchaEnabled = ConfigConstants.getDeleteCaptcha();
|
||||||
|
String expectedPassword = captchaEnabled ?
|
||||||
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
WebUtils.getSessionAttr(request, CAPTCHA_CODE) :
|
||||||
ConfigConstants.getPassword();
|
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);
|
logger.error("删除文件【{}】失败,密码错误!", fileName);
|
||||||
return ReturnResponse.failure("删除文件失败,密码错误!");
|
return ReturnResponse.failure("删除文件失败,密码错误!");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,8 +369,8 @@
|
|||||||
$("#deleteCaptchaConfirmBtn").click(function() {
|
$("#deleteCaptchaConfirmBtn").click(function() {
|
||||||
var fileName = $("#deleteCaptchaFileName").val();
|
var fileName = $("#deleteCaptchaFileName").val();
|
||||||
var deleteCaptchaText = $("#deleteCaptchaText").val();
|
var deleteCaptchaText = $("#deleteCaptchaText").val();
|
||||||
$.get('${baseUrl}deleteFile?fileName=' + fileName +'&password=' + deleteCaptchaText, function(data){
|
$.post('${baseUrl}deleteFile', {fileName: fileName, password: deleteCaptchaText}, function(data){
|
||||||
if ("删除文件失败,密码错误!" === data.msg) {
|
if (!data.success) {
|
||||||
alert(data.msg);
|
alert(data.msg);
|
||||||
} else {
|
} else {
|
||||||
$('#table').bootstrapTable("refresh", {});
|
$('#table').bootstrapTable("refresh", {});
|
||||||
@@ -392,11 +392,16 @@
|
|||||||
function deleteFile(fileName, isFolder) {
|
function deleteFile(fileName, isFolder) {
|
||||||
var message = isFolder ? '你确定要删除这个文件夹吗?(包含所有子文件)' : '你确定要删除这个文件吗?';
|
var message = isFolder ? '你确定要删除这个文件夹吗?(包含所有子文件)' : '你确定要删除这个文件吗?';
|
||||||
if (window.confirm(message)) {
|
if (window.confirm(message)) {
|
||||||
password = prompt("请输入默认密码:123456");
|
var password = prompt("请输入文件删除密码");
|
||||||
|
if (password === null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: '${baseUrl}deleteFile?fileName=' + fileName +'&password='+password,
|
url: '${baseUrl}deleteFile',
|
||||||
|
type: 'POST',
|
||||||
|
data: {fileName: fileName, password: password},
|
||||||
success: function (data) {
|
success: function (data) {
|
||||||
if ("删除文件失败,密码错误!" === data.msg) {
|
if (!data.success) {
|
||||||
alert(data.msg);
|
alert(data.msg);
|
||||||
} else {
|
} else {
|
||||||
$("#table").bootstrapTable("refresh", {});
|
$("#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