Compare commits

..

5 Commits

Author SHA1 Message Date
kl
257180eb95 fix: refresh ImageIO plugins before PDF conversion 2026-08-11 12:54:50 +08:00
kl
e134689df3 Merge pull request #771 from kekingcn/release/5.0.1
Release v5.0.1
2026-07-13 17:16:27 +08:00
kl
3a08031929 chore(release): prepare v5.0.1 2026-07-13 17:12:40 +08:00
kl
47745e4d74 Merge commit from fork 2026-07-13 16:53:58 +08:00
kl
332a98b6fa Add security policy (#770) 2026-06-29 14:58:29 +08:00
10 changed files with 315 additions and 17 deletions

View File

@@ -1,4 +1,4 @@
FROM keking/kkfileview-base:5.0.0
ADD server/target/kkFileView-*.tar.gz /opt/
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.0/bin
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"]
ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.1/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"]

View File

@@ -149,6 +149,29 @@ 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 增加 LinuxWindowsmacOS 构建验证
4. 新增仓库安全策略和私密漏洞报告入口
#### 升级说明
1. 建议所有 v5.0.0 及更早版本用户尽快升级到 v5.0.1
2. 本版本继续要求 JDK 21 及以上现有 v5.0.0 配置可直接沿用
#### > 2026年04月14日v5.0.0 版本发布
#### 优化内容
1. xlsx 前端解析优化 - 提升Excel文件前端渲染性能

View File

@@ -65,6 +65,29 @@ URL[https://file.kkview.cn](https://file.kkview.cn)
## 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)
#### Improvements

View File

@@ -6,7 +6,7 @@
<groupId>cn.keking</groupId>
<artifactId>kkFileView-parent</artifactId>
<version>5.0.0</version>
<version>5.0.1</version>
<properties>
<!-- ========== Java 和编译配置 ========== -->

View File

@@ -6,7 +6,7 @@
<parent>
<artifactId>kkFileView-parent</artifactId>
<groupId>cn.keking</groupId>
<version>5.0.0</version>
<version>5.0.1</version>
</parent>
<artifactId>kkFileView</artifactId>

View File

@@ -18,6 +18,7 @@ import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import javax.imageio.ImageIO;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
@@ -93,6 +94,8 @@ public class PdfToJpgService {
@PostConstruct
public void init() {
refreshImageIoPlugins();
int maxThreads = ConfigConstants.getPdfMaxThreads();
// 使用固定大小的虚拟线程池
this.virtualThreadExecutor = Executors.newFixedThreadPool(maxThreads,
@@ -104,6 +107,13 @@ public class PdfToJpgService {
scheduleCacheCleanup();
}
static void refreshImageIoPlugins() {
// ImageIO only scans once automatically. If another launcher or Java agent initializes
// it before Spring Boot installs its application class loader, nested JAR providers such
// as jbig2-imageio remain invisible until the application class path is scanned again.
ImageIO.scanForPlugins();
}
@PreDestroy
public void shutdown() {
logger.info("开始关闭PDF转换服务...");
@@ -852,4 +862,4 @@ public class PdfToJpgService {
}
}
}

View File

@@ -29,6 +29,7 @@ import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.InvalidPathException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.BasicFileAttributes;
@@ -341,13 +342,23 @@ public class FileController {
}
// ==================== 2. 构建路径和验证 ====================
String basePath = fileDir + demoPath;
if (!ObjectUtils.isEmpty(path)) {
basePath += path + File.separator;
Path currentDir;
try {
currentDir = resolveDirectoryUnderRoot(Paths.get(fileDir, demoDir), path);
} 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;
}
File currentDir = new File(basePath);
if (!currentDir.exists() || !currentDir.isDirectory()) {
if (!Files.isDirectory(currentDir)) {
result.put("total", 0);
result.put("data", Collections.emptyList());
return result;
@@ -357,13 +368,13 @@ public class FileController {
List<Path> allPaths = new ArrayList<>();
long collectStartTime = System.currentTimeMillis();
try (DirectoryStream<Path> stream = Files.newDirectoryStream(Paths.get(basePath))) {
try (DirectoryStream<Path> stream = Files.newDirectoryStream(currentDir)) {
for (Path entry : stream) {
allPaths.add(entry);
stats.incrementFileCount();
}
} catch (IOException e) {
logger.error("读取目录失败: {}", basePath, e);
logger.error("读取目录失败: {}", currentDir, e);
result.put("total", 0);
result.put("data", Collections.emptyList());
return result;
@@ -492,6 +503,46 @@ public class FileController {
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;
}
/**
* 构建性能统计信息
*/
@@ -760,4 +811,4 @@ public class FileController {
File file = new File(fullPath + fileName);
return file.exists();
}
}
}

View File

@@ -41,10 +41,10 @@
你可以先看最新版本的升级重点,再顺着时间轴继续了解历史版本细节。
</p>
<div class="release-badge-row">
<span class="tag highlight">最新版本 v5.0.0</span>
<span class="tag brand">发布日期 2026-04-14</span>
<span class="tag highlight">最新版本 v5.0.1</span>
<span class="tag brand">发布日期 2026-07-13</span>
<span class="tag warn">JDK 21+ 强制要求</span>
<span class="tag">压缩包工作区预览 / PDF 默认模式</span>
<span class="tag">安全补丁 / PDF、Redis、XLSX 修复</span>
</div>
</div>
</section>
@@ -52,11 +52,53 @@
<section class="release-section">
<div class="timeline-year">2026</div>
<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">
<h3>v5.0.0</h3>
<div class="release-meta">
<span class="tag brand">2026-04-14</span>
<span class="tag highlight">最新稳定版本</span>
<span class="tag">5.0 功能版本</span>
<span class="tag warn">升级需 JDK 21+</span>
</div>
<div class="release-columns">

View File

@@ -0,0 +1,51 @@
package cn.keking.service;
import org.junit.jupiter.api.Test;
import javax.imageio.ImageIO;
import javax.imageio.spi.IIORegistry;
import javax.imageio.spi.ImageReaderSpi;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Iterator;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;
class PdfToJpgServiceTests {
@Test
void shouldRediscoverJbig2ReaderAfterInitialRegistryMiss() {
IIORegistry registry = IIORegistry.getDefaultInstance();
List<ImageReaderSpi> providers = findJbig2Providers(registry);
assertFalse(providers.isEmpty(), "jbig2-imageio must be present on the test class path");
try {
providers.forEach(registry::deregisterServiceProvider);
assertFalse(hasJbig2Reader());
PdfToJpgService.refreshImageIoPlugins();
assertTrue(hasJbig2Reader());
} finally {
providers.forEach(registry::registerServiceProvider);
}
}
private static List<ImageReaderSpi> findJbig2Providers(IIORegistry registry) {
Iterator<ImageReaderSpi> providers = registry.getServiceProviders(
ImageReaderSpi.class,
provider -> Arrays.stream(((ImageReaderSpi) provider).getFormatNames())
.anyMatch("JBIG2"::equalsIgnoreCase),
true
);
List<ImageReaderSpi> result = new ArrayList<>();
providers.forEachRemaining(result::add);
return result;
}
private static boolean hasJbig2Reader() {
return ImageIO.getImageReadersByFormatName("JBIG2").hasNext();
}
}

View File

@@ -0,0 +1,98 @@
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());
}
}