Skip to content
Merged
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
4 changes: 3 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,9 @@ jobs:
# package (#44); `--msi` / `--instLocation PerMachine` are solidly
# supported on the 1.2.0 line (bundled WiX 5). Mirrors ws-scrcpy-web's pin.
- name: Install vpk CLI
run: dotnet tool install -g vpk --version 1.2.0
# Source-pinned to nuget.org + signature-validated via the repo nuget.config
# (audit #15: nuget.org-only source + signatureValidationMode=require).
run: dotnet tool install -g vpk --version 1.2.0 --configfile nuget.config

# Restore + build per project directly (not via ControlMenu.sln) to
# mirror the local-pack.ps1 approach (3x dotnet publish below).
Expand Down
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- **Traced SVGs are no longer served as navigable same-origin content.** The Tracing tool wrote its generated SVG under `wwwroot/temp` and pointed both the preview `<img>` and the Download Copy link at that `/temp/<id>.svg` URL. An SVG fetched from a same-origin URL is active content — an embedded `<script>` executes in the app's origin — so that URL was a stored-XSS primitive (reachable if the user opened or navigated to it). The preview and download now use an inert `data:image/svg+xml` URL (script-safe inside `<img>`, and forced to download rather than inline-rendered via the link's `download` attribute), and no SVG is written under `wwwroot/temp`. The SVG-Rasterize tool was reviewed too and is unaffected — it only writes its rasterized PNG/ICO output there, which is inert.
- **The Jellyfin docker-compose parser bounds its input and validates the config path.** It read the compose file with no size limit and accepted whatever host path was mounted at `/config` — a path that then flows into `Path.Combine` and the sqlite3 cast/crew update. It now rejects a compose file larger than 1 MB before reading it, and rejects a `/config` host path that isn't a plain fully-qualified path (no quotes or control characters). A relative or Unix-style path that couldn't resolve to a local `jellyfin.db` on this host now returns a clear error instead of a silently-wrong path.
- **Camera/ONVIF XML responses are parsed with a hardened reader (closes an entity-expansion DoS).** The ONVIF SOAP, WS-Discovery, and Hikvision ISAPI responses were parsed with `XDocument.Parse`. Contrary to the common "DTDs are prohibited by default" guidance — which applies to `XmlReader.Create`, **not** to `XDocument.Parse`/`Load(string)` — `Parse` allows DTDs and expands internal entities on .NET 10 (verified). A malicious device answering discovery could feed a billion-laughs-style payload (bounded by the framework's default entity cap, but still a real amplification); external entities were not fetched by default, so file-exfil XXE was not reachable. All three responses now route through one shared `SafeXml.Parse` that sets `DtdProcessing.Prohibit` (no DTD, no entity expansion), a null `XmlResolver`, and a ~1 MB document cap — closing the DoS and hardening against any future XXE regression.
- **NuGet restores and the `vpk` install are source- and signature-pinned.** A repo `nuget.config` clears package sources to nuget.org only — so a stray machine/user feed or a typosquatting source can't substitute a malicious `vpk` (or any other package) — and sets `signatureValidationMode=require` with the nuget.org repository signer trusted, so every restored package must carry a valid nuget.org signature. The `vpk` install in `release.yml` and `local-pack.ps1` passes `--configfile` so the global-tool install is pinned the same way.
- **The build's `.7z` extractor is vendored instead of taken from `PATH`.** `_Fetcher.ps1` extracted ImageMagick's portable `.7z` by shelling out to whatever `7z` happened to be on the runner's `PATH`; it now fetches a SHA-256-pinned `7zr.exe` into the build cache and extracts with that (Local-Dependencies-Only — build tools are vendored/fetched, never assumed present on the runner). 7-Zip publishes no signature or checksum for the standalone extractor, so the pinned hash is recorded from the official download and fails closed on any change.
- **The in-app updater pins its release channel.** `VelopackUpdateService` now requests the `stable` channel explicitly — the channel `release.yml` publishes non-prerelease tags to — instead of relying on Velopack's RID-derived default, so the updater reliably matches the published stable releases and can't drift onto another channel. (Pinning the update package's publisher/signature is separate, future work — the Velopack Phase-2 signing initiative.)

## [1.2.0] - 2026-06-11

Expand Down
29 changes: 29 additions & 0 deletions nuget.config
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Supply-chain pin for NuGet restores + `dotnet tool install vpk` (audit #15).

- packageSources is cleared to nuget.org ONLY, so a stray machine/user feed (or a
typosquatting source) can't substitute a malicious `vpk` (or any other package).
- signatureValidationMode=require rejects any package that is not signed by a trusted
signer. nuget.org repository-signs every package it serves, so the <trustedSigners>
entry below (the nuget.org repository signer, populated authoritatively via
`dotnet nuget trust source`) covers vpk and every other restored package.

This applies repo-wide (every `dotnet restore` / tool install run from the repo).
-->
<configuration>
<packageSources>
<clear />
<add key="nuget.org" value="https://api.nuget.org/v3/index.json" protocolVersion="3" />
</packageSources>
<config>
<add key="signatureValidationMode" value="require" />
</config>
<trustedSigners>
<repository name="nuget.org" serviceIndex="https://api.nuget.org/v3/index.json">
<certificate fingerprint="0e5f38f57dc1bcc806d8494f4f90fbcedd988b46760709cbeec6f4219aa6157d" hashAlgorithm="SHA256" allowUntrustedRoot="false" />
<certificate fingerprint="5a2901d6ada3d18260b9c6dfe2133c95d74b9eef6ae0e5dc334c8454d1477df4" hashAlgorithm="SHA256" allowUntrustedRoot="false" />
<certificate fingerprint="1f4b311d9acc115c8dc8018b5a49e00fce6da8e2855f9f014ca6f34570bc482d" hashAlgorithm="SHA256" allowUntrustedRoot="false" />
</repository>
</trustedSigners>
</configuration>
38 changes: 24 additions & 14 deletions scripts/dependencies/_Fetcher.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -82,28 +82,38 @@ function Expand-CmZip {
Expand-Archive -LiteralPath $Archive -DestinationPath $DestDir -Force
}

function Get-Cm7zr {
# Resolves a vendored, SHA-pinned 7zr.exe -- the official 7-Zip standalone .7z extractor --
# fetched into the build cache rather than resolved from PATH. Local-Dependencies-Only: build
# tools are vendored/fetched into the app's own folder, never assumed present on the runner.
#
# Provenance note: 7-zip.org serves only the *latest* standalone extractor at this URL and
# publishes neither an Authenticode signature nor a checksum for it, so the SHA-256 below is
# recorded from the official download (trust-on-first-use). It still pins integrity: a tampered
# download fails the hash check, and a future 7-Zip release that changes 7zr.exe trips the same
# check fail-closed -- re-download, confirm provenance, and re-pin the SHA then.
$url = 'https://www.7-zip.org/a/7zr.exe'
$sha256 = 'abcf64ae1cbafddb5395e4cdd3bdc7e3e0561d54a0c6380e3dd43bdbffe519a2'
$cache = Get-CmCacheDir -Name '7zr' -Version 'latest'
$exe = Join-Path $cache '7zr.exe'
Invoke-CmDownload -Url $url -DestFile $exe -ExpectedSha256 $sha256
return $exe
}

function Expand-Cm7z {
param(
[Parameter(Mandatory)][string] $Archive,
[Parameter(Mandatory)][string] $DestDir
)
# Some deps (ImageMagick) ship .7z portables. Expand-Archive can't read .7z,
# so shell out to 7-Zip. CI (windows-latest) has 7z on PATH; locally we fall
# back to the default install dir. 7z is a BUILD tool (like vpk / dotnet),
# not a bundled app dependency, so a system 7z is acceptable for staging.
$sevenZip = (Get-Command 7z -ErrorAction SilentlyContinue).Source
if (-not $sevenZip) {
$candidate = Join-Path $env:ProgramFiles '7-Zip\7z.exe'
if (Test-Path $candidate) { $sevenZip = $candidate }
}
if (-not $sevenZip) {
throw "7-Zip not found. CI runners have 7z on PATH; install 7-Zip locally to extract .7z deps."
}
# Some deps (ImageMagick) ship .7z portables that Expand-Archive can't read. Extract them with
# the vendored, SHA-pinned 7zr.exe fetched into the build cache (Get-Cm7zr) -- never a
# PATH-resolved 7z (Local-Dependencies-Only: a system 7z is no longer assumed present).
$sevenZip = Get-Cm7zr
if (Test-Path $DestDir) { Remove-Item -LiteralPath $DestDir -Recurse -Force }
New-Item -ItemType Directory -Path $DestDir -Force | Out-Null
Write-Host " extracting : $Archive -> $DestDir (7z)"
Write-Host " extracting : $Archive -> $DestDir (7zr)"
& $sevenZip x $Archive "-o$DestDir" -y -bso0 -bsp0
if ($LASTEXITCODE -ne 0) { throw "7z extraction failed (exit $LASTEXITCODE): $Archive" }
if ($LASTEXITCODE -ne 0) { throw "7zr extraction failed (exit $LASTEXITCODE): $Archive" }
}

function Copy-CmStage {
Expand Down
7 changes: 5 additions & 2 deletions scripts/local-pack.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@
# in the csprojs.
#
# Output: Releases/ControlMenu-<version>-Setup.msi + delta/full nupkgs +
# RELEASES.win.json feed. The MSI installs PerMachine to
# releases.stable.json feed (Velopack per-channel naming; this script packs
# --channel stable). The MSI installs PerMachine to
# C:\Program Files\ControlMenu\ with a UAC elevation prompt.

param(
Expand Down Expand Up @@ -75,7 +76,9 @@ function Resolve-Vpk {
Write-Host "Installing vpk $vpkVersion globally (matches the Velopack NuGet pin)..."
# Uninstall any older vpk first to avoid version conflict
dotnet tool uninstall -g vpk 2>$null | Out-Null
dotnet tool install -g vpk --version $vpkVersion
# Source-pinned to nuget.org + signature-validated via the repo nuget.config
# (audit #15: nuget.org-only source + signatureValidationMode=require).
dotnet tool install -g vpk --version $vpkVersion --configfile (Join-Path $PSScriptRoot '..\nuget.config')
if ($LASTEXITCODE -ne 0) { throw "vpk install failed (exit $LASTEXITCODE)" }
Write-Host "vpk $vpkVersion installed."
}
Expand Down
9 changes: 8 additions & 1 deletion src/ControlMenu/Services/Update/VelopackUpdateService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,14 @@ public VelopackUpdateService(IHostApplicationLifetime lifetime, ILogger<Velopack
{
_lifetime = lifetime;
_log = log;
_manager = new UpdateManager(new GithubSource(GitHubRepo, accessToken: null, prerelease: false));
// Pin the update channel to "stable" — the channel release.yml packs non-prerelease tags
// into (`vpk pack --channel stable`; -beta/-alpha tags go to the "beta" channel). Without an
// explicit channel the updater falls back to Velopack's RID-derived default, which would not
// reliably match the published "stable" channel. (Publisher/signature pinning beyond the
// channel is deferred to the Velopack Phase-2 signing initiative.)
_manager = new UpdateManager(
new GithubSource(GitHubRepo, accessToken: null, prerelease: false),
new UpdateOptions { ExplicitChannel = "stable" });
}

public async Task<UpdateAvailability> CheckForUpdatesAsync(CancellationToken ct = default)
Expand Down
Loading