mirror of
https://gitee.com/kekingcn/file-online-preview.git
synced 2026-09-13 08:24:55 +00:00
Compare commits
1 Commits
v5.0.1
...
codex/add-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7323447468 |
@@ -1,4 +1,4 @@
|
|||||||
FROM keking/kkfileview-base:5.0.0
|
FROM keking/kkfileview-base:5.0.0
|
||||||
ADD server/target/kkFileView-*.tar.gz /opt/
|
ADD server/target/kkFileView-*.tar.gz /opt/
|
||||||
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.1/bin
|
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.0/bin
|
||||||
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.1/config/application.properties","-jar","/opt/kkFileView-5.0.1/bin/kkFileView-5.0.1.jar"]
|
ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.0/config/application.properties","-jar","/opt/kkFileView-5.0.0/bin/kkFileView-5.0.0.jar"]
|
||||||
|
|||||||
23
README.cn.md
23
README.cn.md
@@ -149,29 +149,6 @@ pdf预览模式预览效果如下
|
|||||||
|
|
||||||
### 历史更新记录
|
### 历史更新记录
|
||||||
|
|
||||||
#### > 2026年07月13日,v5.0.1 补丁版本发布 :
|
|
||||||
|
|
||||||
#### 安全修复
|
|
||||||
1. 修复 `/addTask` 未经过信任主机和本地目录过滤,可能导致服务端请求伪造(SSRF)的问题(GHSA-gwwj-52hv-6g2m)
|
|
||||||
2. 修复 `/listFiles` 的 `directory` 参数可越出演示目录,造成路径遍历和目录信息泄露的问题(GHSA-pmp8-g8p2-p6jq)
|
|
||||||
|
|
||||||
#### 修复问题
|
|
||||||
1. 修复 PDF 跨域、页码定位、文本高亮、打印和打印水印相关问题
|
|
||||||
2. 修复 PDF 在反向代理场景下的绝对路径问题,以及水印和高亮内容包含特殊字符时的解析失败
|
|
||||||
3. 修复 Redis 单机、集群、主从、哨兵模式配置不一致和地址协议缺失问题
|
|
||||||
4. 修复下载 MIME 类型校验失败后仍返回成功、HTTP 错误原因不明确,以及共享 HTTP Client 被错误关闭的问题
|
|
||||||
5. 修复 LuckyExcel 数据校验类型未映射时的 xlsx 解析崩溃
|
|
||||||
|
|
||||||
#### 优化内容
|
|
||||||
1. 大型 xlsx 文件改用 Web Worker 执行 LuckyExcel 解析,并在 Worker 不可用或异常时自动回退主线程
|
|
||||||
2. 新增 `pdf.sidebar.open` 配置,可控制 PDF 预览是否默认打开侧栏
|
|
||||||
3. Maven CI 增加 Linux、Windows、macOS 构建验证
|
|
||||||
4. 新增仓库安全策略和私密漏洞报告入口
|
|
||||||
|
|
||||||
#### 升级说明
|
|
||||||
1. 建议所有 v5.0.0 及更早版本用户尽快升级到 v5.0.1
|
|
||||||
2. 本版本继续要求 JDK 21 及以上,现有 v5.0.0 配置可直接沿用
|
|
||||||
|
|
||||||
#### > 2026年04月14日,v5.0.0 版本发布 :
|
#### > 2026年04月14日,v5.0.0 版本发布 :
|
||||||
#### 优化内容
|
#### 优化内容
|
||||||
1. xlsx 前端解析优化 - 提升Excel文件前端渲染性能
|
1. xlsx 前端解析优化 - 提升Excel文件前端渲染性能
|
||||||
|
|||||||
23
README.md
23
README.md
@@ -65,29 +65,6 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
|
|||||||
|
|
||||||
## Change History
|
## Change History
|
||||||
|
|
||||||
### Version 5.0.1 (July 13, 2026)
|
|
||||||
|
|
||||||
#### Security Fixes
|
|
||||||
1. Fixed `/addTask` bypassing trusted-host and local-directory filters, which could allow server-side request forgery (SSRF) (GHSA-gwwj-52hv-6g2m)
|
|
||||||
2. Fixed the `/listFiles` `directory` parameter escaping the demo directory, which could allow path traversal and directory information disclosure (GHSA-pmp8-g8p2-p6jq)
|
|
||||||
|
|
||||||
#### Fixes
|
|
||||||
1. Fixed PDF cross-origin access, page positioning, text highlighting, printing, and print watermark issues
|
|
||||||
2. Fixed PDF absolute paths behind reverse proxies and parsing failures when watermark or highlight text contains special characters
|
|
||||||
3. Fixed inconsistent Redis settings across standalone, cluster, master-replica, and sentinel modes, including missing address protocols
|
|
||||||
4. Fixed successful responses after MIME validation failures, unclear HTTP error reporting, and accidental closure of a shared HTTP client
|
|
||||||
5. Fixed xlsx parsing crashes when LuckyExcel data-validation types have no mapping
|
|
||||||
|
|
||||||
#### Improvements
|
|
||||||
1. Moved LuckyExcel parsing for large xlsx files into a Web Worker, with automatic main-thread fallback when the Worker is unavailable or fails
|
|
||||||
2. Added `pdf.sidebar.open` to control whether the PDF sidebar opens by default
|
|
||||||
3. Added Linux, Windows, and macOS validation to Maven CI
|
|
||||||
4. Added a repository security policy and private vulnerability reporting guidance
|
|
||||||
|
|
||||||
#### Upgrade Notes
|
|
||||||
1. All users running v5.0.0 or earlier are strongly encouraged to upgrade to v5.0.1
|
|
||||||
2. JDK 21 or higher remains required, and existing v5.0.0 configuration can be reused
|
|
||||||
|
|
||||||
### Version 5.0.0 (April 14, 2026)
|
### Version 5.0.0 (April 14, 2026)
|
||||||
|
|
||||||
#### Improvements
|
#### Improvements
|
||||||
|
|||||||
2
pom.xml
2
pom.xml
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<version>5.0.1</version>
|
<version>5.0.0</version>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
<!-- ========== Java 和编译配置 ========== -->
|
<!-- ========== Java 和编译配置 ========== -->
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<artifactId>kkFileView-parent</artifactId>
|
<artifactId>kkFileView-parent</artifactId>
|
||||||
<groupId>cn.keking</groupId>
|
<groupId>cn.keking</groupId>
|
||||||
<version>5.0.1</version>
|
<version>5.0.0</version>
|
||||||
</parent>
|
</parent>
|
||||||
|
|
||||||
<artifactId>kkFileView</artifactId>
|
<artifactId>kkFileView</artifactId>
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import java.io.InputStream;
|
|||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.nio.file.DirectoryStream;
|
import java.nio.file.DirectoryStream;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
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;
|
||||||
@@ -342,23 +341,13 @@ public class FileController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ==================== 2. 构建路径和验证 ====================
|
// ==================== 2. 构建路径和验证 ====================
|
||||||
Path currentDir;
|
String basePath = fileDir + demoPath;
|
||||||
try {
|
if (!ObjectUtils.isEmpty(path)) {
|
||||||
currentDir = resolveDirectoryUnderRoot(Paths.get(fileDir, demoDir), path);
|
basePath += path + File.separator;
|
||||||
} catch (InvalidPathException | SecurityException e) {
|
|
||||||
logger.warn("拒绝访问 demo 目录之外的文件列表路径");
|
|
||||||
result.put("total", 0);
|
|
||||||
result.put("data", Collections.emptyList());
|
|
||||||
result.put("error", "非法目录路径");
|
|
||||||
return result;
|
|
||||||
} catch (IOException e) {
|
|
||||||
logger.error("解析 demo 目录失败", e);
|
|
||||||
result.put("total", 0);
|
|
||||||
result.put("data", Collections.emptyList());
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Files.isDirectory(currentDir)) {
|
File currentDir = new File(basePath);
|
||||||
|
if (!currentDir.exists() || !currentDir.isDirectory()) {
|
||||||
result.put("total", 0);
|
result.put("total", 0);
|
||||||
result.put("data", Collections.emptyList());
|
result.put("data", Collections.emptyList());
|
||||||
return result;
|
return result;
|
||||||
@@ -368,13 +357,13 @@ public class FileController {
|
|||||||
List<Path> allPaths = new ArrayList<>();
|
List<Path> allPaths = new ArrayList<>();
|
||||||
long collectStartTime = System.currentTimeMillis();
|
long collectStartTime = System.currentTimeMillis();
|
||||||
|
|
||||||
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) {
|
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(basePath))) {
|
||||||
for (Path entry : stream) {
|
for (Path entry : stream) {
|
||||||
allPaths.add(entry);
|
allPaths.add(entry);
|
||||||
stats.incrementFileCount();
|
stats.incrementFileCount();
|
||||||
}
|
}
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
logger.error("读取目录失败: {}", currentDir, e);
|
logger.error("读取目录失败: {}", basePath, e);
|
||||||
result.put("total", 0);
|
result.put("total", 0);
|
||||||
result.put("data", Collections.emptyList());
|
result.put("data", Collections.emptyList());
|
||||||
return result;
|
return result;
|
||||||
@@ -503,46 +492,6 @@ public class FileController {
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Resolve an existing directory below the configured demo root.
|
|
||||||
*
|
|
||||||
* <p>Both lexical normalization and real-path checks are required: the
|
|
||||||
* former blocks traversal and absolute paths, while the latter prevents a
|
|
||||||
* symlink inside the demo directory from escaping the configured root.</p>
|
|
||||||
*/
|
|
||||||
static Path resolveDirectoryUnderRoot(Path root, String requestedPath) throws IOException {
|
|
||||||
Path normalizedRoot = root.toAbsolutePath().normalize();
|
|
||||||
String relativePath = requestedPath == null ? "" : requestedPath.replace('\\', '/');
|
|
||||||
|
|
||||||
if (relativePath.indexOf('\0') >= 0
|
|
||||||
|| relativePath.startsWith("/")
|
|
||||||
|| relativePath.matches("^[A-Za-z]:.*")) {
|
|
||||||
throw new SecurityException("Absolute paths are not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path relative = Paths.get(relativePath);
|
|
||||||
if (relative.isAbsolute()) {
|
|
||||||
throw new SecurityException("Absolute paths are not allowed");
|
|
||||||
}
|
|
||||||
for (Path segment : relative) {
|
|
||||||
if ("..".equals(segment.toString())) {
|
|
||||||
throw new SecurityException("Parent path segments are not allowed");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Path resolved = normalizedRoot.resolve(relative).normalize();
|
|
||||||
if (!resolved.startsWith(normalizedRoot)) {
|
|
||||||
throw new SecurityException("Path escapes the configured root");
|
|
||||||
}
|
|
||||||
|
|
||||||
Path realRoot = normalizedRoot.toRealPath();
|
|
||||||
Path realResolved = resolved.toRealPath();
|
|
||||||
if (!realResolved.startsWith(realRoot)) {
|
|
||||||
throw new SecurityException("Path escapes the configured root through a symbolic link");
|
|
||||||
}
|
|
||||||
return realResolved;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 构建性能统计信息
|
* 构建性能统计信息
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -41,10 +41,10 @@
|
|||||||
你可以先看最新版本的升级重点,再顺着时间轴继续了解历史版本细节。
|
你可以先看最新版本的升级重点,再顺着时间轴继续了解历史版本细节。
|
||||||
</p>
|
</p>
|
||||||
<div class="release-badge-row">
|
<div class="release-badge-row">
|
||||||
<span class="tag highlight">最新版本 v5.0.1</span>
|
<span class="tag highlight">最新版本 v5.0.0</span>
|
||||||
<span class="tag brand">发布日期 2026-07-13</span>
|
<span class="tag brand">发布日期 2026-04-14</span>
|
||||||
<span class="tag warn">JDK 21+ 强制要求</span>
|
<span class="tag warn">JDK 21+ 强制要求</span>
|
||||||
<span class="tag">安全补丁 / PDF、Redis、XLSX 修复</span>
|
<span class="tag">压缩包工作区预览 / PDF 默认模式</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@@ -52,53 +52,11 @@
|
|||||||
<section class="release-section">
|
<section class="release-section">
|
||||||
<div class="timeline-year">2026</div>
|
<div class="timeline-year">2026</div>
|
||||||
<div class="timeline-list">
|
<div class="timeline-list">
|
||||||
<article class="release-card">
|
|
||||||
<h3>v5.0.1</h3>
|
|
||||||
<div class="release-meta">
|
|
||||||
<span class="tag brand">2026-07-13</span>
|
|
||||||
<span class="tag highlight">最新稳定版本</span>
|
|
||||||
<span class="tag warn">建议尽快升级</span>
|
|
||||||
</div>
|
|
||||||
<div class="release-columns">
|
|
||||||
<div class="release-group">
|
|
||||||
<h4>安全修复</h4>
|
|
||||||
<ul class="release-list">
|
|
||||||
<li>修复 <code>/addTask</code> 未覆盖信任主机和本地目录过滤导致的 SSRF 风险。</li>
|
|
||||||
<li>修复 <code>/listFiles</code> 可越出演示目录导致的路径遍历和目录信息泄露。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="release-group">
|
|
||||||
<h4>修复</h4>
|
|
||||||
<ul class="release-list">
|
|
||||||
<li>修复 PDF 跨域、页码、高亮、打印、打印水印及反向代理路径问题。</li>
|
|
||||||
<li>修复 Redis 多种运行模式的配置兼容问题。</li>
|
|
||||||
<li>修复 HTTP 错误处理、共享 Client 生命周期和 xlsx 数据校验解析问题。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="release-group">
|
|
||||||
<h4>优化</h4>
|
|
||||||
<ul class="release-list">
|
|
||||||
<li>大型 xlsx 文件使用 Web Worker 解析,并保留主线程自动回退。</li>
|
|
||||||
<li>新增 <code>pdf.sidebar.open</code>,支持配置 PDF 默认侧栏状态。</li>
|
|
||||||
<li>Maven CI 增加 Linux、Windows、macOS 构建验证。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
<div class="release-group">
|
|
||||||
<h4>升级重点</h4>
|
|
||||||
<ul class="release-list">
|
|
||||||
<li>建议所有 v5.0.0 及更早版本用户尽快升级。</li>
|
|
||||||
<li>继续要求 JDK 21 及以上。</li>
|
|
||||||
<li>现有 v5.0.0 配置可直接沿用。</li>
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</article>
|
|
||||||
|
|
||||||
<article class="release-card">
|
<article class="release-card">
|
||||||
<h3>v5.0.0</h3>
|
<h3>v5.0.0</h3>
|
||||||
<div class="release-meta">
|
<div class="release-meta">
|
||||||
<span class="tag brand">2026-04-14</span>
|
<span class="tag brand">2026-04-14</span>
|
||||||
<span class="tag">5.0 功能版本</span>
|
<span class="tag highlight">最新稳定版本</span>
|
||||||
<span class="tag warn">升级需 JDK 21+</span>
|
<span class="tag warn">升级需 JDK 21+</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="release-columns">
|
<div class="release-columns">
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
package cn.keking.web.controller;
|
|
||||||
|
|
||||||
import cn.keking.config.ConfigConstants;
|
|
||||||
import org.junit.jupiter.api.AfterEach;
|
|
||||||
import org.junit.jupiter.api.Assumptions;
|
|
||||||
import org.junit.jupiter.api.BeforeEach;
|
|
||||||
import org.junit.jupiter.api.Test;
|
|
||||||
import org.junit.jupiter.api.io.TempDir;
|
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.nio.file.Files;
|
|
||||||
import java.nio.file.Path;
|
|
||||||
import java.util.List;
|
|
||||||
import java.util.Map;
|
|
||||||
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
|
||||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
||||||
|
|
||||||
class FileControllerPathSecurityTests {
|
|
||||||
|
|
||||||
@TempDir
|
|
||||||
Path tempDir;
|
|
||||||
|
|
||||||
private String originalFileDir;
|
|
||||||
|
|
||||||
@BeforeEach
|
|
||||||
void rememberConfiguredFileDirectory() {
|
|
||||||
originalFileDir = ConfigConstants.getFileDir();
|
|
||||||
}
|
|
||||||
|
|
||||||
@AfterEach
|
|
||||||
void restoreConfiguredFileDirectory() {
|
|
||||||
ConfigConstants.setFileDirValue(originalFileDir);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldResolveDirectoriesInsideDemoRoot() throws IOException {
|
|
||||||
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
|
||||||
Path nested = Files.createDirectories(demoRoot.resolve("folder/subfolder"));
|
|
||||||
|
|
||||||
assertEquals(demoRoot.toRealPath(), FileController.resolveDirectoryUnderRoot(demoRoot, ""));
|
|
||||||
assertEquals(nested.toRealPath(), FileController.resolveDirectoryUnderRoot(demoRoot, "folder/subfolder"));
|
|
||||||
assertEquals(nested.toRealPath(), FileController.resolveDirectoryUnderRoot(demoRoot, "folder\\subfolder"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectParentTraversalWithEitherSeparator() throws IOException {
|
|
||||||
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
|
||||||
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "../outside"));
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "..\\outside"));
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "folder/../outside"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectAbsoluteDriveAndUncPaths() throws IOException {
|
|
||||||
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
|
||||||
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "/etc"));
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "C:\\Windows"));
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "\\\\server\\share"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void shouldRejectSymlinkThatEscapesDemoRoot() throws IOException {
|
|
||||||
Path demoRoot = Files.createDirectory(tempDir.resolve("demo"));
|
|
||||||
Path outside = Files.createDirectory(tempDir.resolve("outside"));
|
|
||||||
Path link = demoRoot.resolve("outside-link");
|
|
||||||
try {
|
|
||||||
Files.createSymbolicLink(link, outside);
|
|
||||||
} catch (IOException | UnsupportedOperationException e) {
|
|
||||||
Assumptions.assumeTrue(false, "Symbolic links are unavailable in this environment");
|
|
||||||
}
|
|
||||||
|
|
||||||
assertThrows(SecurityException.class,
|
|
||||||
() -> FileController.resolveDirectoryUnderRoot(demoRoot, "outside-link"));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Test
|
|
||||||
void listFilesShouldNotExposeEntriesOutsideDemoRoot() throws IOException {
|
|
||||||
Files.createDirectory(tempDir.resolve("demo"));
|
|
||||||
Files.createFile(tempDir.resolve("outside-secret.txt"));
|
|
||||||
ConfigConstants.setFileDirValue(tempDir.toString());
|
|
||||||
FileController controller = new FileController();
|
|
||||||
|
|
||||||
Map<String, Object> result = controller.getFiles("..", "", 0, 20, null, null);
|
|
||||||
|
|
||||||
assertEquals("非法目录路径", result.get("error"));
|
|
||||||
assertTrue(((List<?>) result.get("data")).isEmpty());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user