Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 39 additions & 0 deletions SECURITY_CONFIG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`,两者二选一;启动日志会说明当前由谁决定客户端地址。

## 🚨 安全事件响应

如果发现可疑的预览请求:
Expand Down
5 changes: 5 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@
<!-- ========== 安全相关 ========== -->
<bcprov-jdk15on.version>1.70</bcprov-jdk15on.version>

<!-- ========== 应用层防护 (Guard Core,默认关闭,见 SECURITY_CONFIG.md) ========== -->
<spring-boot-guard.version>1.3.0</spring-boot-guard.version>
<kotlin-bom.version>2.4.21</kotlin-bom.version>
<kotlinx-coroutines-bom.version>1.11.0</kotlinx-coroutines-bom.version>

<!-- ========== 其他工具库 ========== -->
<xstream.version>1.4.21</xstream.version>
<antlr.version>2.7.7</antlr.version>
Expand Down
23 changes: 23 additions & 0 deletions server/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@

<dependencyManagement>
<dependencies>
<!-- Kotlin / coroutines BOM 必须先于 Spring Boot BOM 导入:Spring Boot 3.5 默认管理 Kotlin 1.9.x 和 coroutines 1.8.x,
而 spring-boot-guard 需要 Kotlin 2.x 和 coroutines 1.11.0 -->
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-bom</artifactId>
<version>${kotlin-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlinx</groupId>
<artifactId>kotlinx-coroutines-bom</artifactId>
<version>${kotlinx-coroutines-bom.version}</version>
<scope>import</scope>
<type>pom</type>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
Expand Down Expand Up @@ -59,6 +75,13 @@
<optional>true</optional>
</dependency>

<!-- ========== 应用层防护 (默认关闭,guard.enabled=true 时生效) ========== -->
<dependency>
<groupId>com.guard-core</groupId>
<artifactId>spring-boot-guard</artifactId>
<version>${spring-boot-guard.version}</version>
</dependency>

<!-- ========== 文档格式转换 ========== -->
<dependency>
<groupId>org.jodconverter</groupId>
Expand Down
86 changes: 86 additions & 0 deletions server/src/main/config/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -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:}
15 changes: 12 additions & 3 deletions server/src/main/java/cn/keking/ServerMain.java
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Loading