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 - 最新稳定版本 + 上一补丁版本 建议尽快升级