From 7d311ee02fa48cb63ac93bc5d7d97133a62c8bce Mon Sep 17 00:00:00 2001
From: kl
Date: Thu, 8 Oct 2026 15:39:17 +0800
Subject: [PATCH] release: prepare v5.0.3 security fixes and verified
deployment
---
.github/scripts/deploy_windows_winrm.py | 2 +
.github/scripts/remote_windows_deploy.ps1 | 135 +++++++++++++++---
.github/scripts/test_windows_deploy.ps1 | 42 ++++++
.github/workflows/master-auto-deploy.yml | 7 +
.github/workflows/maven.yml | 12 ++
Dockerfile | 4 +-
README.cn.md | 16 +++
README.md | 16 +++
pom.xml | 2 +-
server/pom.xml | 2 +-
server/src/main/resources/web/main/record.ftl | 36 ++++-
11 files changed, 248 insertions(+), 26 deletions(-)
create mode 100644 .github/scripts/test_windows_deploy.ps1
diff --git a/.github/scripts/deploy_windows_winrm.py b/.github/scripts/deploy_windows_winrm.py
index 93f79454..c86f20be 100644
--- a/.github/scripts/deploy_windows_winrm.py
+++ b/.github/scripts/deploy_windows_winrm.py
@@ -40,6 +40,8 @@ def main() -> int:
"KK_DEPLOY_MVN_CMD": optional_env("KK_DEPLOY_MVN_CMD", r"C:\kkFileView-tools\maven\bin\mvn.cmd"),
"KK_DEPLOY_MAVEN_SETTINGS": optional_env("KK_DEPLOY_MAVEN_SETTINGS", ""),
"KK_DEPLOY_DRY_RUN": optional_env("KK_DEPLOY_DRY_RUN", "false").lower(),
+ "KK_DEPLOY_REVISION": optional_env("KK_DEPLOY_REVISION", ""),
+ "KK_DEPLOY_HARDEN_ACTUATOR": optional_env("KK_DEPLOY_HARDEN_ACTUATOR", "false").lower(),
}
script_path = pathlib.Path(__file__).with_name("remote_windows_deploy.ps1")
diff --git a/.github/scripts/remote_windows_deploy.ps1 b/.github/scripts/remote_windows_deploy.ps1
index bc0901ed..2a6828e1 100644
--- a/.github/scripts/remote_windows_deploy.ps1
+++ b/.github/scripts/remote_windows_deploy.ps1
@@ -41,6 +41,8 @@ $GitExe = Get-OptionalEnv 'KK_DEPLOY_GIT_EXE' 'C:\kkFileView-tools\git\cmd\git.e
$MvnCmd = Get-OptionalEnv 'KK_DEPLOY_MVN_CMD' 'C:\kkFileView-tools\maven\bin\mvn.cmd'
$MavenSettings = Get-OptionalEnv 'KK_DEPLOY_MAVEN_SETTINGS' ''
$DryRun = Get-OptionalEnv 'KK_DEPLOY_DRY_RUN' 'false'
+$ExpectedRevision = Get-OptionalEnv 'KK_DEPLOY_REVISION' ''
+$HardenActuator = (Get-OptionalEnv 'KK_DEPLOY_HARDEN_ACTUATOR' 'false') -eq 'true'
$BinDir = Join-Path $DeployRoot 'bin'
$StartupScript = Join-Path $BinDir 'startup.bat'
@@ -202,6 +204,18 @@ function Build-KkFileView {
}
Sync-Repository
+if (-not [string]::IsNullOrWhiteSpace($ExpectedRevision)) {
+ if ($ExpectedRevision -notmatch '^[0-9a-f]{40}$') {
+ throw 'Deployment revision must be a full commit SHA'
+ }
+ Invoke-External -FilePath $GitExe -Arguments @('fetch', '--depth', '1', 'origin', $ExpectedRevision) -WorkingDirectory $SourceRoot
+ Invoke-External -FilePath $GitExe -Arguments @('checkout', '--detach', $ExpectedRevision) -WorkingDirectory $SourceRoot
+}
+$SourceRevision = (& $GitExe -C $SourceRoot rev-parse HEAD).Trim()
+if ($LASTEXITCODE -ne 0 -or ($ExpectedRevision -and $SourceRevision -ne $ExpectedRevision)) {
+ throw 'Source revision does not match the requested deployment commit'
+}
+Write-Step "Building source revision: $SourceRevision"
Build-KkFileView
$DownloadedJars = Get-ChildItem $BuildOutputDir -Filter 'kkFileView-*.jar' -File
@@ -295,33 +309,118 @@ function Wait-Health {
return $false
}
+function Resolve-ActiveConfigPath {
+ param([string[]]$CommandLines, [string]$WorkingDirectory)
+ $Paths = @()
+ foreach ($CommandLine in $CommandLines) {
+ $MatchesFound = [regex]::Matches($CommandLine, '(?:-D|--)spring\.config\.location=(?:"([^"]+)"|([^\s"<>]+))')
+ foreach ($Match in $MatchesFound) {
+ $Value = $Match.Groups[1].Value
+ if (-not $Value) { $Value = $Match.Groups[2].Value }
+ $Value = $Value -replace '^file:', ''
+ if ($Value -match '[,;*?%$]' -or $Value -notmatch '\.properties$') {
+ throw 'Security config migration requires one explicit .properties file'
+ }
+ if (-not [System.IO.Path]::IsPathRooted($Value)) {
+ $Value = Join-Path $WorkingDirectory $Value
+ }
+ $Paths += [System.IO.Path]::GetFullPath($Value)
+ }
+ }
+ $Paths = @($Paths | Select-Object -Unique)
+ if ($Paths.Count -ne 1 -or -not (Test-Path -LiteralPath $Paths[0] -PathType Leaf)) {
+ throw 'Unable to identify a unique active external configuration file'
+ }
+ return $Paths[0]
+}
+
+function Set-SecureActuatorDefaults {
+ param([string]$ConfigPath)
+ # Round-trip unrelated bytes unchanged, regardless of the existing encoding.
+ $Encoding = [System.Text.Encoding]::GetEncoding(28591)
+ $Content = $Encoding.GetString([System.IO.File]::ReadAllBytes($ConfigPath))
+ $NewLine = "`n"
+ if ($Content.Contains("`r`n")) { $NewLine = "`r`n" }
+ foreach ($Entry in @(
+ @('management.endpoints.web.exposure.include', 'health'),
+ @('management.endpoint.health.show-details', 'never')
+ )) {
+ $Pattern = '(?m)^[ \t]*' + [regex]::Escape($Entry[0]) + '[ \t]*[=:][^\r\n]*'
+ $Line = $Entry[0] + ' = ' + $Entry[1]
+ if ([regex]::IsMatch($Content, $Pattern)) {
+ $Content = [regex]::Replace($Content, $Pattern, $Line)
+ } else {
+ $Content += $NewLine + $Line + $NewLine
+ }
+ }
+ [System.IO.File]::WriteAllBytes($ConfigPath, $Encoding.GetBytes($Content))
+}
+
+function Assert-SecureActuatorExposure {
+ param([string]$Url)
+ $HealthEndpoint = [System.Uri]::new([System.Uri]$Url, 'actuator/health').AbsoluteUri
+ $MetricsEndpoint = [System.Uri]::new([System.Uri]$Url, 'actuator/metrics').AbsoluteUri
+ $Health = Invoke-RestMethod -Uri $HealthEndpoint -TimeoutSec 10
+ if ($Health.status -ne 'UP' -or @($Health.PSObject.Properties).Count -ne 1) {
+ throw 'Health endpoint still exposes component details or is unhealthy'
+ }
+ try {
+ $null = Invoke-WebRequest -Uri $MetricsEndpoint -UseBasicParsing -TimeoutSec 10
+ throw 'Metrics endpoint remains publicly exposed'
+ } catch {
+ if (-not $_.Exception.Response -or [int]$_.Exception.Response.StatusCode -ne 404) {
+ throw
+ }
+ }
+ Write-Step 'Security verification passed: aggregate health only; metrics returns 404'
+}
+
+$ActiveConfig = $null
+$BackupConfig = $null
+if ($HardenActuator) {
+ $CommandLines = @(Get-KkFileViewJavaProcesses | ForEach-Object { $_.CommandLine })
+ if ($CommandLines.Count -eq 0) { $CommandLines = @(Get-Content -LiteralPath $StartupScript -Raw) }
+ $ActiveConfig = Resolve-ActiveConfigPath -CommandLines $CommandLines -WorkingDirectory $BinDir
+ $BackupConfig = Join-Path $ReleaseDir ("{0}.{1}.bak" -f [System.IO.Path]::GetFileName($ActiveConfig), $Timestamp)
+ Write-Step "Active configuration: $ActiveConfig"
+ Write-Step "Backing up active configuration to $BackupConfig"
+ Copy-Item -LiteralPath $ActiveConfig -Destination $BackupConfig
+}
+
Write-Step "Backing up current jar to $BackupJar"
Copy-Item $JarPath $BackupJar -Force
-Stop-KkFileView
-if (-not (Wait-KkFileViewStopped)) {
- throw "Timed out waiting for the previous kkFileView process to exit"
-}
-
-Write-Step "Replacing jar with artifact output"
-Copy-Item $DownloadedJar.FullName $JarPath -Force
-
-Start-KkFileView
-
-if (-not (Wait-Health -Url $HealthUrl)) {
- Write-Step "Health check failed, rolling back"
+try {
Stop-KkFileView
if (-not (Wait-KkFileViewStopped)) {
- throw "Timed out waiting for the failed kkFileView process to exit during rollback"
+ throw "Timed out waiting for the previous kkFileView process to exit"
}
- Copy-Item $BackupJar $JarPath -Force
+ if ($HardenActuator) { Set-SecureActuatorDefaults -ConfigPath $ActiveConfig }
+ Write-Step "Replacing jar with artifact output: $($DownloadedJar.Name)"
+ Copy-Item $DownloadedJar.FullName $JarPath -Force
Start-KkFileView
-
if (-not (Wait-Health -Url $HealthUrl)) {
- throw "Deployment failed and rollback health check also failed"
+ throw 'Deployment health check failed'
}
-
- throw "Deployment failed, rollback completed successfully"
+ if ($HardenActuator) { Assert-SecureActuatorExposure -Url $HealthUrl }
+} catch {
+ $DeploymentError = $_
+ Write-Step 'Deployment verification failed, restoring jar and configuration'
+ Stop-KkFileView
+ if (-not (Wait-KkFileViewStopped)) { throw 'Unable to stop the failed deployment for rollback' }
+ Copy-Item $BackupJar $JarPath -Force
+ if ($BackupConfig) { Copy-Item -LiteralPath $BackupConfig -Destination $ActiveConfig -Force }
+ Start-KkFileView
+ if (-not (Wait-Health -Url $HealthUrl)) { throw 'Deployment and rollback health checks both failed' }
+ throw "Deployment failed; rollback succeeded: $DeploymentError"
}
+@{
+ revision = $SourceRevision
+ artifact = $DownloadedJar.Name
+ sha256 = (Get-FileHash -LiteralPath $JarPath -Algorithm SHA256).Hash
+ actuatorHardened = $HardenActuator
+ deployedAt = (Get-Date).ToUniversalTime().ToString('o')
+} | ConvertTo-Json | Set-Content -LiteralPath (Join-Path $DeployRoot 'deployment.json') -Encoding UTF8
+Write-Step "Verified deployed revision: $SourceRevision"
Write-Step "Deployment completed successfully"
diff --git a/.github/scripts/test_windows_deploy.ps1 b/.github/scripts/test_windows_deploy.ps1
new file mode 100644
index 00000000..486cc50f
--- /dev/null
+++ b/.github/scripts/test_windows_deploy.ps1
@@ -0,0 +1,42 @@
+$ErrorActionPreference = 'Stop'
+$Tokens = $null
+$ParseErrors = $null
+$ScriptPath = Join-Path $PSScriptRoot 'remote_windows_deploy.ps1'
+$Ast = [System.Management.Automation.Language.Parser]::ParseFile($ScriptPath, [ref]$Tokens, [ref]$ParseErrors)
+if ($ParseErrors.Count -gt 0) { throw ($ParseErrors | Out-String) }
+foreach ($Name in @('Resolve-ActiveConfigPath', 'Set-SecureActuatorDefaults')) {
+ $FunctionAst = $Ast.Find({ param($Node) $Node -is [System.Management.Automation.Language.FunctionDefinitionAst] -and $Node.Name -eq $Name }, $true)
+ if (-not $FunctionAst) { throw "Missing function: $Name" }
+ . ([scriptblock]::Create($FunctionAst.Extent.Text))
+}
+
+$TempRoot = Join-Path ([System.IO.Path]::GetTempPath()) ('kkfileview-deploy-test-' + [guid]::NewGuid().ToString('N'))
+try {
+ $Bin = New-Item -ItemType Directory -Path (Join-Path $TempRoot 'bin')
+ $Config = New-Item -ItemType Directory -Path (Join-Path $TempRoot 'config')
+ $ActiveFile = Join-Path $Config.FullName 'test.properties'
+ $Utf8 = New-Object System.Text.UTF8Encoding($false)
+ $Original = "# Preserve Unicode: 中文`r`ncustom.value = unchanged`r`nmanagement.endpoints.web.exposure.include = health,info,metrics`r`nmanagement.endpoint.health.show-details = always`r`n"
+ [System.IO.File]::WriteAllText($ActiveFile, $Original, $Utf8)
+ $Resolved = Resolve-ActiveConfigPath -CommandLines @('java -Dspring.config.location=..\config\test.properties -jar kkFileView-5.0.jar') -WorkingDirectory $Bin.FullName
+ if ($Resolved -ne $ActiveFile) { throw 'Did not resolve the runtime configuration override' }
+ Set-SecureActuatorDefaults -ConfigPath $Resolved
+ $Expected = $Original.Replace('health,info,metrics', 'health').Replace('show-details = always', 'show-details = never')
+ $Actual = [System.IO.File]::ReadAllText($Resolved, $Utf8)
+ if ($Actual -cne $Expected) { throw 'Migration altered unrelated configuration or missed a security default' }
+ Set-SecureActuatorDefaults -ConfigPath $Resolved
+ if ([System.IO.File]::ReadAllText($Resolved, $Utf8) -cne $Expected) { throw 'Migration is not idempotent' }
+
+ [System.IO.File]::WriteAllText($ActiveFile, 'custom.value = unchanged', $Utf8)
+ Set-SecureActuatorDefaults -ConfigPath $ActiveFile
+ $Actual = [System.IO.File]::ReadAllText($ActiveFile, $Utf8)
+ if ($Actual -notmatch 'management.endpoints.web.exposure.include = health' -or $Actual -notmatch 'management.endpoint.health.show-details = never') {
+ throw 'Missing properties were not added'
+ }
+ $Rejected = $false
+ try { Resolve-ActiveConfigPath -CommandLines @('java -jar app.jar') -WorkingDirectory $Bin.FullName } catch { $Rejected = $true }
+ if (-not $Rejected) { throw 'Unknown active configuration must fail closed' }
+ Write-Host 'Windows deployment syntax and configuration migration tests passed'
+} finally {
+ if (Test-Path $TempRoot) { Remove-Item -LiteralPath $TempRoot -Recurse -Force }
+}
diff --git a/.github/workflows/master-auto-deploy.yml b/.github/workflows/master-auto-deploy.yml
index cfc0ef72..b1507be9 100644
--- a/.github/workflows/master-auto-deploy.yml
+++ b/.github/workflows/master-auto-deploy.yml
@@ -4,6 +4,11 @@ on:
push:
branches: [ master ]
workflow_dispatch:
+ inputs:
+ harden_actuator:
+ description: Apply v5.0.3 monitoring defaults to the active external config
+ type: boolean
+ default: false
concurrency:
group: master-auto-deploy-production
@@ -29,6 +34,8 @@ jobs:
KK_DEPLOY_GIT_EXE: ${{ vars.KK_DEPLOY_GIT_EXE }}
KK_DEPLOY_MVN_CMD: ${{ vars.KK_DEPLOY_MVN_CMD }}
KK_DEPLOY_MAVEN_SETTINGS: ${{ vars.KK_DEPLOY_MAVEN_SETTINGS }}
+ KK_DEPLOY_REVISION: ${{ github.sha }}
+ KK_DEPLOY_HARDEN_ACTUATOR: ${{ inputs.harden_actuator || 'false' }}
steps:
- name: Checkout repository
diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml
index 1e4c554c..b7e15bda 100644
--- a/.github/workflows/maven.yml
+++ b/.github/workflows/maven.yml
@@ -35,6 +35,18 @@ jobs:
- name: Build with Maven
run: mvn -B package "-Dmaven.test.skip=true" --file pom.xml
+ - name: Security regression tests
+ if: runner.os == 'Linux'
+ run: >-
+ mvn -B -pl server
+ -Dtest=FtpCommandSecurityTests,UploadPathSecurityTests,PreviewSourceSecurityTests,PreviewParameterSecurityTests,ArchiveDirectorySecurityTests,ActuatorExposureSecurityTests,ArchiveTraversalSecurityTests,FileControllerPathSecurityTests,FileControllerDeleteSecurityTests
+ test
+
+ - name: Validate Windows deployment
+ if: runner.os == 'Windows'
+ shell: powershell
+ run: .github/scripts/test_windows_deploy.ps1
+
- name: Upload Linux distribution package
if: success() && runner.os == 'Linux'
uses: actions/upload-artifact@v4
diff --git a/Dockerfile b/Dockerfile
index 165f8a02..6d061808 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -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.2/bin
-ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.2/config/application.properties","-jar","/opt/kkFileView-5.0.2/bin/kkFileView-5.0.2.jar"]
+ENV KKFILEVIEW_BIN_FOLDER=/opt/kkFileView-5.0.3/bin
+ENTRYPOINT ["java","-Dfile.encoding=UTF-8","-Dspring.config.location=/opt/kkFileView-5.0.3/config/application.properties","-jar","/opt/kkFileView-5.0.3/bin/kkFileView-5.0.3.jar"]
diff --git a/README.cn.md b/README.cn.md
index d2332a4e..6a3e8739 100644
--- a/README.cn.md
+++ b/README.cn.md
@@ -149,6 +149,22 @@ pdf预览模式预览效果如下
### 历史更新记录
+#### > 2026年10月08日,v5.0.3 安全补丁版本发布:
+
+#### 安全修复
+1. FTP 连接前校验命令参数,阻止控制字符注入
+2. 上传和建目录限制在演示文件目录内,并检查符号链接
+3. 校验所有预览源地址,阻止通过不同参数绕过信任策略
+4. 预览页码和代理参数经过类型校验后再渲染模板
+5. 在通过校验的压缩包预览中生成目录树,移除公开的 `/directory` 接口
+6. 默认仅开放汇总健康状态,不再默认暴露健康详情、info 和 metrics
+
+#### 升级说明
+1. 建议所有 v5.0.2 及更早版本用户升级至 v5.0.3,继续要求 JDK 21 及以上
+2. 已有外部配置文件需设置 `management.endpoints.web.exposure.include=health` 和 `management.endpoint.health.show-details=never`,才能应用监控配置加固
+3. `/directory` 接口已移除,内置压缩包目录预览继续可用
+4. 上传仍默认禁用;启用后,上传和建目录仅允许演示目录内的路径
+
#### > 2026年08月14日,v5.0.2 补丁版本发布 :
#### 安全修复
diff --git a/README.md b/README.md
index 1284f36d..22fce91e 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,22 @@ URL:[https://file.kkview.cn](https://file.kkview.cn)
## Change History
+### Version 5.0.3 (October 8, 2026)
+
+#### Security Fixes
+1. Reject control characters in FTP command arguments before connecting
+2. Confine uploads and folder creation to demo storage, including symbolic-link checks
+3. Validate every preview source URL to prevent parameter-confusion bypasses of trust policies
+4. Validate page and agent parameters before rendering preview templates
+5. Render archive directory trees within the validated preview flow and remove the public `/directory` endpoint
+6. Expose only aggregate health status by default; detailed health, info, and metrics are no longer public defaults
+
+#### Upgrade Notes
+1. All v5.0.2 and earlier users should upgrade to v5.0.3; JDK 21 or higher remains required
+2. Existing external configuration files must set `management.endpoints.web.exposure.include=health` and `management.endpoint.health.show-details=never` to apply the monitoring hardening
+3. The `/directory` endpoint is removed; the built-in archive preview remains supported
+4. Uploads remain disabled by default. Enabled uploads and folder creation accept only paths beneath demo storage
+
### Version 5.0.2 (August 14, 2026)
#### Security Fixes
diff --git a/pom.xml b/pom.xml
index 7c46e332..ebdcf33d 100644
--- a/pom.xml
+++ b/pom.xml
@@ -6,7 +6,7 @@
cn.keking
kkFileView-parent
- 5.0.2
+ 5.0.3
diff --git a/server/pom.xml b/server/pom.xml
index b1679048..0ab81334 100644
--- a/server/pom.xml
+++ b/server/pom.xml
@@ -6,7 +6,7 @@
kkFileView-parent
cn.keking
- 5.0.2
+ 5.0.3
kkFileView
diff --git a/server/src/main/resources/web/main/record.ftl b/server/src/main/resources/web/main/record.ftl
index 149d7ded..6ade251b 100644
--- a/server/src/main/resources/web/main/record.ftl
+++ b/server/src/main/resources/web/main/record.ftl
@@ -41,10 +41,10 @@
你可以先看最新版本的升级重点,再顺着时间轴继续了解历史版本细节。
- 最新版本 v5.0.2
- 发布日期 2026-08-14
+ 最新版本 v5.0.3
+ 发布日期 2026-10-08
JDK 21+ 强制要求
- 安全补丁 / HTML、文件删除、PDF 转图修复
+ 安全补丁 / FTP、文件路径、预览与监控加固
@@ -52,11 +52,39 @@
2026
+
+ v5.0.3
+
+ 2026-10-08
+ 最新稳定版本
+ 建议尽快升级
+
+
+
+
安全修复
+
+ - 校验 FTP 命令参数,限制上传和建目录路径。
+ - 校验所有预览源地址,以及页码和代理参数。
+ - 压缩包目录树改由通过校验的预览生成,移除公开的目录枚举接口。
+ - 监控默认仅开放汇总健康状态。
+
+
+
+
升级重点
+
+ - 建议 v5.0.2 及更早版本用户升级,继续要求 JDK 21 及以上。
+ - 已有外部配置需将
management.endpoints.web.exposure.include 设为 health,将 management.endpoint.health.show-details 设为 never。
+ /directory 接口已移除,内置压缩包预览继续可用。
+
+
+
+
+
v5.0.2
2026-08-14
- 最新稳定版本
+ 上一补丁版本
建议尽快升级