diff --git a/SECURITY_CONFIG.md b/SECURITY_CONFIG.md
index ad96085bb..3c1caec3c 100644
--- a/SECURITY_CONFIG.md
+++ b/SECURITY_CONFIG.md
@@ -155,6 +155,45 @@ trust.host = *.example.com
- `*.example.com` 会匹配 `cdn.example.com`、`api.internal.example.com`,但不匹配根域 `example.com`
- 对于 IP 风格通配(如 `192.168.*`、`10.*`),仅匹配字面量 IPv4 地址,不匹配域名
+## 🧱 应用层防护 Guard Core(可选,默认关闭)
+
+kkFileView 可以选择启用 [Guard Core](https://github.com/Guard-Core/spring-boot-guard)(spring-boot-guard,MIT),在应用内增加请求频率限制、攻击特征检测和自动封禁。它**不替代** `trust.host` / `not.trust.host` 和各个安全修复,只限制谁能访问这些接口、访问多快,并封禁持续探测的客户端。
+
+Guard Core is an optional, off-by-default application-layer guard (rate limits, attack screening, auto-ban). It does not replace `trust.host` / `not.trust.host` or the code fixes for the advisories. It limits who can reach the routes, how fast, and bans clients that keep probing.
+
+### 启用 / Enable
+
+```properties
+guard.enabled = true
+# 或环境变量 / or: KK_GUARD_ENABLED=true
+```
+
+不设置 `guard.enabled` 或设置为 `false` 时,不会注册任何 Guard 组件,行为与之前完全一致。建议先用 `KK_GUARD_PASSIVE_MODE=true`(只记录不拦截)观察一段时间,再关闭观察模式。完整配置见 `application.properties` 的“十五、Guard Core 应用层防护”。
+
+### 它做什么 / What it does
+
+| 风险 / Risk | 措施 / Measure | 配置 / Setting |
+|---|---|---|
+| 转换、预览、远程读取、入队被刷(LibreOffice 转换消耗大量 CPU) | 每个客户端的全局和分接口请求限制,超限返回 429 | `guard.rate-limit`、`guard.endpoint-rate-limits[...]` |
+| 目录穿越、注入、XSS、扫描探测(路径、查询参数、请求头、请求体,包含 Base64 解码后的 url) | 攻击特征检测,返回 400,持续触发的客户端自动封禁 | `guard.enabled-detection-categories`、`guard.auto-ban-*` |
+| 反复读取不存在的远程文件 | 同一接口累计大量 404 的客户端封禁 | `guard.global-behavior-rules[0]` |
+| 删除密码被猜解 | 删除失败超过 5 次的客户端封禁 1 小时 | `FileController#deleteFile` 上的 `@GuardReturnMonitor` |
+| 上传请求过大或类型不对 | `/fileUpload` 只接受 `multipart/form-data`,Content-Length 超过 500MB 直接返回 413 | `FileController#fileUpload` 上的注解 |
+| 写操作接口被匿名调用(上传、创建文件夹、删除、addTask) | IP 白名单和/或密钥(请求头 `X-KK-Admin-Key`),同时设置时两者都必须满足 | `KK_GUARD_ADMIN_ALLOWED_IPS`、`KK_GUARD_ADMIN_KEY` |
+
+### 诚实的限制 / Honest limits
+
+- 不检测 `ssrf` 类别:预览内网地址是正常用法,SSRF 仍由 `trust.host` / `not.trust.host` 负责。Guard 会解码 Base64 的 `url` 并检测解码后的内容,但 AES 加密(`encryption=aes`)的 `url` 无法检测。
+- 默认不启用 `sensitive_file` 和 `recon` 类别:它们会把 `.java`、`.py`、`.sql`、`.sh`、`.jsp`、`.asp` 等源码文件的访问路径当作探测,而 kkFileView 会预览这类文件。可以用 `KK_GUARD_DETECTION_CATEGORIES` 覆盖。
+- 使用 `local.preview.dir`(`file://` 预览)时,需要把 `url`、`urlPath`、`urls`、`currentUrl` 加入 `guard.excluded-detection-params`,否则 `file://` 会被当作文件包含攻击。
+- 404 规则按接口统计,不是按路径统计:遍历大量不同静态路径的扫描器只会被请求限制限速,不会被封禁。
+- `/fileUpload` 的 multipart 请求体不做检测(`scanBody = false`):spring-boot-guard 1.3.0 会检测表单字段和文件部分的内容,而 kkFileView 会保存并预览 html、sql、sh 等源码,这类文件会被当作攻击拒绝。路径、查询参数和请求头仍然检测,`path` 表单字段由 `FileController` 校验。Guard 看不到上传文件部分的内容,`Content-Type` 检查的是请求而不是文件部分,文件类型仍由 `prohibit` 控制。声明的 Content-Length 达到 2GB 或以上时,引擎的大小检查会抛出异常并返回 500(请求仍然被拒绝)。
+- 引擎不会封禁回环地址(127.0.0.1):来自回环地址的探测会被拒绝,但不会被封禁。
+- 计数和封禁默认保存在进程内存中:多实例之间不共享,重启后清空。需要共享时设置 `KK_GUARD_ENABLE_REDIS=true` 和 `KK_GUARD_REDIS_URL`(独立于 `cache.type=redis`)。
+- 全局限制默认 600 次/分钟:预览页面会加载大量静态资源,限制按客户端地址统计,办公室出口共用一个地址时请调大 `KK_GUARD_RATE_LIMIT`。
+- 响应头和 CORS 保持关闭:kkFileView 常被嵌入 iframe,Guard 的 `X-Frame-Options` 会破坏嵌入。
+- 反向代理之后必须设置 `KK_GUARD_TRUSTED_PROXIES`(代理的 IP/CIDR),否则所有客户端共用代理的地址。只信任来自这些地址的 `X-Forwarded-For`。不要同时设置 `server.forward-headers-strategy=native/framework`,两者二选一;启动日志会说明当前由谁决定客户端地址。
+
## 🚨 安全事件响应
如果发现可疑的预览请求:
diff --git a/pom.xml b/pom.xml
index ebdcf33d1..2899d076f 100644
--- a/pom.xml
+++ b/pom.xml
@@ -64,6 +64,11 @@
1.70
+
+ 1.3.0
+ 2.4.21
+ 1.11.0
+
1.4.21
2.7.7
diff --git a/server/pom.xml b/server/pom.xml
index 0ab813349..062837090 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -13,6 +13,22 @@
+
+
+ org.jetbrains.kotlin
+ kotlin-bom
+ ${kotlin-bom.version}
+ import
+ pom
+
+
+ org.jetbrains.kotlinx
+ kotlinx-coroutines-bom
+ ${kotlinx-coroutines-bom.version}
+ import
+ pom
+
org.springframework.boot
spring-boot-dependencies
@@ -59,6 +75,13 @@
true
+
+
+ com.guard-core
+ spring-boot-guard
+ ${spring-boot-guard.version}
+
+
org.jodconverter
diff --git a/server/src/main/config/application.properties b/server/src/main/config/application.properties
index 9bc1bd9e7..d86dd8bc8 100644
--- a/server/src/main/config/application.properties
+++ b/server/src/main/config/application.properties
@@ -480,3 +480,89 @@ kk.scriptjs = false
# 纯文本文件类型,直接显示
simText = ${KK_SIMTEXT:txt,html,htm,asp,jsp,xml,json,properties,md,gitignore,log,java,py,c,cpp,sql,sh,bat,m,bas,prg,cmd}
+
+
+###############################################################################
+# 十五、Guard Core 应用层防护(可选,默认关闭,需要重启生效)
+###############################################################################
+# 开启后在应用内增加:每个客户端的访问频率限制、对路径/查询参数/请求头的攻击特征检测(目录穿越、注入、XSS、扫描探测)
+# 并自动封禁持续探测的客户端、上传请求限制,以及对写操作接口的 IP 白名单/密钥保护。
+# 它不替代 trust.host / not.trust.host(SSRF 防护)和各个安全修复,只限制谁能访问、访问多快,并封禁探测者。
+# 注意:Guard 会解码 Base64 参数(url / urlPath / urls / fileName)并检测解码后的内容,但 AES 加密(encryption=aes)的 url 无法检测。
+# 域名、内网地址的校验仍由 TrustHostFilter 完成。
+# 详见 SECURITY_CONFIG.md
+
+# 总开关(spring-boot-guard 的 guard.enabled,库的默认值是 true,这里改为默认 false):false 时不会注册任何 Guard 组件,行为与之前完全一致
+guard.enabled = ${KK_GUARD_ENABLED:false}
+
+# 仅记录不拦截(观察模式):先用 true 上线,确认没有误报后再改为 false
+guard.passive-mode = ${KK_GUARD_PASSIVE_MODE:false}
+
+# 反向代理(nginx 等)的 IP/CIDR,多个用逗号隔开。只信任来自这些地址的 X-Forwarded-For,其他来源一律使用 TCP 连接地址。
+# 在反向代理之后部署时必须设置,否则所有客户端会共用代理的地址。
+# 不要同时设置 server.forward-headers-strategy=native/framework,二选一
+guard.trusted-proxies = ${KK_GUARD_TRUSTED_PROXIES:}
+
+# 默认使用进程内存保存计数和封禁(多实例之间不共享,重启后清空)。
+# 多实例部署需要共享时,设置为 true 并配置 Redis 地址(lettuce 格式,如 redis://:password@127.0.0.1:6379/0)
+guard.enable-redis = ${KK_GUARD_ENABLE_REDIS:false}
+guard.redis-url = ${KK_GUARD_REDIS_URL:redis://127.0.0.1:6379}
+
+# 每个客户端的全局请求限制:窗口(秒)内最多请求数。预览页面会加载大量静态资源,所以全局值较大
+guard.enable-rate-limiting = true
+guard.rate-limit = ${KK_GUARD_RATE_LIMIT:600}
+guard.rate-limit-window = 60
+
+# 消耗 CPU 的接口单独限制(转换文档、读取远程文件、入队转换、上传、删除),格式:窗口 60 秒内的最大请求数
+guard.endpoint-rate-limits[/onlinePreview].requests = ${KK_GUARD_RATE_LIMIT_PREVIEW:60}
+guard.endpoint-rate-limits[/onlinePreview].window = 60
+guard.endpoint-rate-limits[/picturesPreview].requests = ${KK_GUARD_RATE_LIMIT_PREVIEW:60}
+guard.endpoint-rate-limits[/picturesPreview].window = 60
+guard.endpoint-rate-limits[/getCorsFile].requests = ${KK_GUARD_RATE_LIMIT_CORSFILE:120}
+guard.endpoint-rate-limits[/getCorsFile].window = 60
+guard.endpoint-rate-limits[/addTask].requests = ${KK_GUARD_RATE_LIMIT_ADDTASK:30}
+guard.endpoint-rate-limits[/addTask].window = 60
+guard.endpoint-rate-limits[/fileUpload].requests = ${KK_GUARD_RATE_LIMIT_WRITE:10}
+guard.endpoint-rate-limits[/fileUpload].window = 60
+guard.endpoint-rate-limits[/createFolder].requests = ${KK_GUARD_RATE_LIMIT_WRITE:10}
+guard.endpoint-rate-limits[/createFolder].window = 60
+guard.endpoint-rate-limits[/deleteFile].requests = ${KK_GUARD_RATE_LIMIT_WRITE:10}
+guard.endpoint-rate-limits[/deleteFile].window = 60
+
+# 攻击特征检测(路径、查询参数、请求头、请求体)。默认类别不包含:
+# ssrf:预览内网地址(url=http://192.168...)是 kkFileView 的正常用法,SSRF 由 trust.host / not.trust.host 负责
+# sensitive_file、recon:会把 .java .py .sql .sh .jsp .asp 等源码文件的访问路径当作探测,而 kkFileView 会预览这类文件
+# (例如压缩包内或 demo 目录中的源码文件),误报会导致这些文件无法预览。大量遍历不同路径的扫描由全局请求限制限速
+# 需要调整时,通过环境变量 KK_GUARD_DETECTION_CATEGORIES 覆盖(逗号分隔)
+guard.enable-penetration-detection = true
+guard.enabled-detection-categories = ${KK_GUARD_DETECTION_CATEGORIES:xss,sqli,dir_traversal,path_traversal,cmd_injection,file_inclusion,ldap,xml,nosql,file_upload,template,http_split,cms_probing,proto_pollution,code_injection,deserialization}
+
+# 持续触发检测的客户端自动封禁:累计次数达到阈值后封禁指定秒数
+guard.enable-ip-banning = true
+guard.auto-ban-threshold = ${KK_GUARD_AUTO_BAN_THRESHOLD:10}
+guard.auto-ban-duration = ${KK_GUARD_AUTO_BAN_DURATION:3600}
+
+# fileName 是 Base64 编码的 file://localhost/文件名(演示页的删除请求),解码后会被当作 file:// 文件包含,需要排除;
+# 文件名本身仍由 FileController 校验。如果使用 local.preview.dir(file:// 预览),还需要把 url、urlPath、urls、currentUrl 加入 guard.excluded-detection-params
+guard.excluded-detection-body-fields = fileName
+
+# 行为规则:同一接口上累计大量 404 的客户端封禁 1 小时(按接口统计,不是按路径统计)。
+# 它能封禁反复读取不存在的远程文件(/getCorsFile)的客户端,但不能封禁遍历大量不同静态路径的扫描器,那类扫描由全局请求限制限制速度
+guard.global-behavior-rules[0].rule-type = return_pattern
+guard.global-behavior-rules[0].pattern = status:404
+guard.global-behavior-rules[0].threshold = ${KK_GUARD_BAN_404_THRESHOLD:50}
+guard.global-behavior-rules[0].window = 300
+guard.global-behavior-rules[0].action = ban
+guard.global-behavior-rules[0].ban-duration = 3600
+# 删除接口按响应内容统计失败次数(删除密码错误),只缓冲该接口的响应
+guard.behavior-scan-response-body = true
+
+# 响应头和 CORS 保持关闭:kkFileView 常被嵌入 iframe,Guard 的 X-Frame-Options 等响应头会破坏嵌入
+guard.security-headers.enabled = false
+guard.enable-cors = false
+
+# 写操作接口(上传、创建文件夹、删除、addTask)的访问控制,默认都为空(不限制):
+# 允许访问的 IP/CIDR,多个用逗号隔开,例如 127.0.0.1,10.0.0.0/8
+guard.kk.admin-allowed-ips = ${KK_GUARD_ADMIN_ALLOWED_IPS:}
+# 访问密钥,调用方在请求头 X-KK-Admin-Key 中携带。同时设置时两个条件都必须满足
+guard.kk.admin-key = ${KK_GUARD_ADMIN_KEY:}
diff --git a/server/src/main/java/cn/keking/ServerMain.java b/server/src/main/java/cn/keking/ServerMain.java
index 0e5b52988..0fb413014 100644
--- a/server/src/main/java/cn/keking/ServerMain.java
+++ b/server/src/main/java/cn/keking/ServerMain.java
@@ -18,12 +18,21 @@ public class ServerMain {
private static final Logger logger = LoggerFactory.getLogger(ServerMain.class);
+ /**
+ * spring-boot-guard is on unless {@code guard.enabled=false}. The shipped application.properties turns it off, but
+ * {@code -Dspring.config.location} replaces that file with the operator's own copy, so the opt-in default is repeated
+ * here as the lowest-precedence property: any config file, environment variable or argument still wins.
+ */
+ static SpringApplicationBuilder applicationBuilder() {
+ return new SpringApplicationBuilder(ServerMain.class)
+ .logStartupInfo(false)
+ .properties("guard.enabled=false");
+ }
+
public static void main(String[] args) {
StopWatch stopWatch = new StopWatch();
stopWatch.start();
- ConfigurableApplicationContext context = new SpringApplicationBuilder(ServerMain.class)
- .logStartupInfo(false)
- .run(args);
+ ConfigurableApplicationContext context = applicationBuilder().run(args);
stopWatch.stop();
ServerProperties serverProperties = context.getBean(ServerProperties.class);
Integer port = serverProperties.getPort();
diff --git a/server/src/main/java/cn/keking/guard/GuardAdminAccessValidator.java b/server/src/main/java/cn/keking/guard/GuardAdminAccessValidator.java
new file mode 100644
index 000000000..d352cf107
--- /dev/null
+++ b/server/src/main/java/cn/keking/guard/GuardAdminAccessValidator.java
@@ -0,0 +1,144 @@
+package cn.keking.guard;
+
+import com.guardcore.protocols.GuardRequest;
+import com.guardcore.protocols.GuardResponse;
+import com.guardcore.spring.CustomRouteValidator;
+import kotlin.coroutines.Continuation;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Component;
+
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.nio.charset.StandardCharsets;
+import java.security.MessageDigest;
+import java.util.ArrayList;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.regex.Pattern;
+
+/**
+ * Limits who may reach the state-changing endpoints (upload, folder creation, delete, addTask).
+ * Both checks are optional and off by default: an IP allowlist ({@code guard.kk.admin-allowed-ips})
+ * and an API key ({@code guard.kk.admin-key}, sent in the {@code X-KK-Admin-Key} header).
+ * When both are set, both must pass. The client IP is the one Guard Core resolved, so it honours
+ * {@code guard.trusted-proxies} and ignores a spoofed X-Forwarded-For from any other peer.
+ */
+@Component(GuardAdminAccessValidator.BEAN_NAME)
+@ConditionalOnProperty(name = "guard.enabled", havingValue = "true")
+public class GuardAdminAccessValidator implements CustomRouteValidator {
+
+ public static final String BEAN_NAME = "kkGuardAdminAccessValidator";
+ static final String KEY_HEADER = "x-kk-admin-key";
+ private static final Pattern IPV4_LITERAL = Pattern.compile("(\\d{1,3}\\.){3}\\d{1,3}");
+ private static final Pattern IPV6_LITERAL = Pattern.compile("[0-9a-fA-F:.]*:[0-9a-fA-F:.]*");
+
+ private final List allowedNetworks = new ArrayList<>();
+ private final byte[] adminKey;
+
+ public GuardAdminAccessValidator(@Value("${guard.kk.admin-allowed-ips:}") List allowedIps,
+ @Value("${guard.kk.admin-key:}") String adminKey) {
+ for (String entry : allowedIps) {
+ if (!entry.isBlank()) {
+ allowedNetworks.add(Network.parse(entry.trim()));
+ }
+ }
+ this.adminKey = adminKey.getBytes(StandardCharsets.UTF_8);
+ }
+
+ @Override
+ public Object validate(GuardRequest request, Continuation super Object> continuation) {
+ if (isIpAllowed(request) && isKeyAccepted(request)) {
+ return null;
+ }
+ return forbidden();
+ }
+
+ private boolean isIpAllowed(GuardRequest request) {
+ if (allowedNetworks.isEmpty()) {
+ return true;
+ }
+ Object clientIp = request.getState().getAttr("client_ip");
+ return clientIp instanceof String ip && allowedNetworks.stream().anyMatch(network -> network.contains(ip));
+ }
+
+ private boolean isKeyAccepted(GuardRequest request) {
+ if (adminKey.length == 0) {
+ return true;
+ }
+ String supplied = request.getHeaders().get(KEY_HEADER);
+ return supplied != null && MessageDigest.isEqual(adminKey, supplied.getBytes(StandardCharsets.UTF_8));
+ }
+
+ private static GuardResponse forbidden() {
+ byte[] body = "Forbidden".getBytes(StandardCharsets.UTF_8);
+ Map headers = new LinkedHashMap<>();
+ headers.put("Content-Type", "text/plain; charset=utf-8");
+ headers.put("Content-Length", String.valueOf(body.length));
+ return new GuardResponse() {
+ @Override public int getStatusCode() { return 403; }
+ @Override public Map getHeaders() { return headers; }
+ @Override public byte[] getBody() { return body; }
+ };
+ }
+
+ /** An IP or CIDR literal. Typos fail startup instead of silently widening or narrowing access. */
+ record Network(byte[] address, int prefixLength) {
+
+ static Network parse(String entry) {
+ String[] parts = entry.split("/", -1);
+ byte[] address = parts.length <= 2 ? literal(parts[0]) : null;
+ if (address == null) {
+ throw invalid(entry);
+ }
+ int prefix = address.length * 8;
+ if (parts.length == 2) {
+ try {
+ prefix = Integer.parseInt(parts[1]);
+ } catch (NumberFormatException e) {
+ throw invalid(entry);
+ }
+ if (prefix < 0 || prefix > address.length * 8) {
+ throw invalid(entry);
+ }
+ }
+ return new Network(address, prefix);
+ }
+
+ boolean contains(String ip) {
+ byte[] candidate = literal(ip);
+ if (candidate == null || candidate.length != address.length) {
+ return false;
+ }
+ int fullBytes = prefixLength / 8;
+ for (int i = 0; i < fullBytes; i++) {
+ if (candidate[i] != address[i]) {
+ return false;
+ }
+ }
+ int remainingBits = prefixLength % 8;
+ if (remainingBits == 0) {
+ return true;
+ }
+ int mask = 0xFF << (8 - remainingBits);
+ return (candidate[fullBytes] & mask) == (address[fullBytes] & mask);
+ }
+
+ /** Only literals reach InetAddress, so this never performs a DNS lookup. IPv4-mapped IPv6 becomes IPv4. */
+ private static byte[] literal(String value) {
+ if (!IPV4_LITERAL.matcher(value).matches() && !IPV6_LITERAL.matcher(value).matches()) {
+ return null;
+ }
+ try {
+ return InetAddress.getByName(value).getAddress();
+ } catch (UnknownHostException e) {
+ return null;
+ }
+ }
+
+ private static IllegalArgumentException invalid(String entry) {
+ return new IllegalArgumentException("guard.kk.admin-allowed-ips: invalid entry '" + entry + "'");
+ }
+ }
+}
diff --git a/server/src/main/java/cn/keking/guard/GuardClientIdentityReport.java b/server/src/main/java/cn/keking/guard/GuardClientIdentityReport.java
new file mode 100644
index 000000000..eaa88bc13
--- /dev/null
+++ b/server/src/main/java/cn/keking/guard/GuardClientIdentityReport.java
@@ -0,0 +1,48 @@
+package cn.keking.guard;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.boot.cloud.CloudPlatform;
+import org.springframework.boot.context.properties.bind.Bindable;
+import org.springframework.boot.context.properties.bind.Binder;
+import org.springframework.core.env.Environment;
+import org.springframework.stereotype.Component;
+
+import java.util.List;
+
+/**
+ * Logs, once at startup, which layer decides the client address, so Tomcat and Guard Core cannot disagree
+ * about proxy trust unnoticed. Tomcat rewrites the remote address only when server.forward-headers-strategy
+ * is native or framework (or a cloud platform that implies it); Guard Core trusts X-Forwarded-For only from
+ * guard.trusted-proxies. Use one of the two, not both.
+ */
+@Component
+@ConditionalOnProperty(name = "guard.enabled", havingValue = "true")
+public class GuardClientIdentityReport {
+
+ private static final Logger logger = LoggerFactory.getLogger(GuardClientIdentityReport.class);
+
+ public GuardClientIdentityReport(Environment environment) {
+ List trustedProxies = Binder.get(environment)
+ .bind("guard.trusted-proxies", Bindable.listOf(String.class)).orElse(List.of());
+ String strategy = environment.getProperty("server.forward-headers-strategy");
+ CloudPlatform platform = CloudPlatform.getActive(environment);
+ boolean tomcatRewrites = strategy != null
+ ? !"none".equalsIgnoreCase(strategy)
+ : platform != null && platform.isUsingForwardHeaders();
+ if (tomcatRewrites && !trustedProxies.isEmpty()) {
+ logger.warn("Guard Core: Tomcat already rewrites the client address from forwarded headers "
+ + "(server.forward-headers-strategy) and guard.trusted-proxies is also set. Use only one of them, "
+ + "and keep server.tomcat.remoteip.internal-proxies as narrow as guard.trusted-proxies.");
+ } else if (tomcatRewrites) {
+ logger.info("Guard Core: client address comes from Tomcat (server.forward-headers-strategy), "
+ + "guard.trusted-proxies is empty.");
+ } else if (trustedProxies.isEmpty()) {
+ logger.info("Guard Core: client address is the TCP peer, X-Forwarded-For is ignored. Behind a reverse proxy "
+ + "set guard.trusted-proxies, otherwise every client shares the proxy address.");
+ } else {
+ logger.info("Guard Core: client address is the TCP peer, X-Forwarded-For is trusted only from {}.", trustedProxies);
+ }
+ }
+}
diff --git a/server/src/main/java/cn/keking/web/controller/FileController.java b/server/src/main/java/cn/keking/web/controller/FileController.java
index 57e77e06c..a1057bee6 100644
--- a/server/src/main/java/cn/keking/web/controller/FileController.java
+++ b/server/src/main/java/cn/keking/web/controller/FileController.java
@@ -1,12 +1,18 @@
package cn.keking.web.controller;
import cn.keking.config.ConfigConstants;
+import cn.keking.guard.GuardAdminAccessValidator;
import cn.keking.model.FileType;
import cn.keking.model.ReturnResponse;
import cn.keking.utils.CaptchaUtil;
import cn.keking.utils.DateUtils;
import cn.keking.utils.KkFileUtils;
import cn.keking.utils.WebUtils;
+import com.guardcore.spring.GuardContentTypeFilter;
+import com.guardcore.spring.GuardCustomValidation;
+import com.guardcore.spring.GuardDetectionExclusion;
+import com.guardcore.spring.GuardMaxRequestSize;
+import com.guardcore.spring.GuardReturnMonitor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.util.ObjectUtils;
@@ -153,6 +159,12 @@ public String getStats() {
}
}
+ // Same ceiling as spring.servlet.multipart.max-request-size, checked on Content-Length before the body is parsed.
+ @GuardContentTypeFilter(allowedTypes = "multipart/form-data")
+ @GuardMaxRequestSize(sizeBytes = 524288000)
+ // Path, query and headers are still scanned. The multipart body is not: spring-boot-guard 1.3.0 scans file part payloads along with form fields, and kkFileView stores and previews html, sql and shell sources that look like attacks.
+ @GuardDetectionExclusion(scanBody = false)
+ @GuardCustomValidation(validatorBean = GuardAdminAccessValidator.BEAN_NAME)
@PostMapping("/fileUpload")
public ReturnResponse