docs(929): the record names what holds each bound, and Renovate is held

security.imagesharp-3x-tiff-decoder-disabled now says the bounds are
enforced by ImageSharpSurfaceTests rather than by its own prose. The
"Load pass is masked" claim is dropped from the record and from the two
code comments that repeated it: with the architecture pin, reverting the
Load pass's options alone now reddens
Only_ImageSharpDecoding_constructs_DecoderOptions (witnessed
2026-10-27), so the claim had become false. The residual it cannot see,
an extension-dispatched Save(path), is named.

Renovate: a packageRule holds SixLabors.ImageSharp on 3.1.x. The
existing "range pins are respected" sentence in ci-cd.md and the EF
group's description were wrong: Dependency Dashboard #22 offers EF Core
v10 and MediatR 14.2.0 over their range pins, so a range does not hold
Renovate. Whether the rule holds the OSV security PR (#923) cannot be
measured before merge, and the record says so.

guard-inventory.md's hand-written C# guard list gains the new fixture
and its two Architecture.Tests siblings, and stops quoting a count.

This branch also edits the rationale prose of
ffmpeg.remote-image-fetcher-bounded (commit 73045865f: the MaxFrames
measurement is dated to 3.1.12 and TIFF is no longer in play), which
the decisions lifecycle check requires this trailer for.

Decisions-Edit: yes
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: f9ce6bc6-5b36-491a-8d72-89c1f70dd3e1
This commit is contained in:
2026-10-27 22:46:57 +01:00
co-authored by Claude Opus 5.5
parent a1f0c492ce
commit d1af9daf53
8 changed files with 57 additions and 20 deletions
@@ -61,8 +61,8 @@ public class ImageSharpDecodingTests
}
// the validator as a whole (render path, save path and upload path). The Identify pass rejects
// first, so the Load pass's own configuration is MASKED here: reverting only that one leaves
// this fixture green. The header test below is what pins the Identify pass.
// a TIFF before the Load pass runs, so this exercises Identify; that the Load pass takes the
// same options is held by ErsatzTV.Architecture.Tests' ImageSharpSurfaceTests.
[Test]
public async Task DecodeAndValidate_Rejects_A_Tiff_As_An_Unknown_Format()
{
@@ -23,7 +23,9 @@ namespace ErsatzTV.Infrastructure.Images;
/// TIFF graphic. It is a fresh instance rather than a <c>Configuration.Default.Clone()</c> because
/// Clone is shallow: removing a detector from the clone would remove it from Default too.
/// <c>ImageSharpDecodingTests</c> pins the module list against <c>Configuration.Default</c>, so a
/// package bump that adds a format fails a test instead of silently dropping it.
/// package bump that adds a format fails a test instead of silently dropping it, and
/// <c>ImageSharpSurfaceTests</c> (ErsatzTV.Architecture.Tests) fails any production decode that does not
/// take these options.
/// Rationale and revisit trigger: <c>security.imagesharp-3x-tiff-decoder-disabled</c>. (ersatztv#929)
/// </remarks>
public static class ImageSharpDecoding
@@ -42,8 +42,8 @@ public class RemoteImageValidator : IRemoteImageValidator
// Without this, adding the header pre-pass would silently disable every animated-PNG logo
// that worked before this change. Only Width/Height are read below, and those stay correct.
// Both passes use ImageSharpDecoding's configuration, which cannot detect TIFF (ersatztv#929).
// This pass is the one that rejects a TIFF, so the decode pass's identical configuration is
// masked by it in tests; it is kept so the two passes always agree on which formats exist.
// This pass is the one that rejects a TIFF; the decode pass takes the same configuration so
// the two always agree on which formats exist (held by ImageSharpSurfaceTests).
ImageInfo info = await Image.IdentifyAsync(
ImageSharpDecoding.Options(maxFrames: 1),
stream,
+5 -2
View File
@@ -2638,8 +2638,11 @@ github.com PAT for changelogs — named `GH_`, **not** `GITHUB_`, a prefix Gitea
**Patch** bumps to test/dev-only packages (NUnit\*, NSubstitute, Shouldly, coverlet,
`Microsoft.NET.Test.Sdk`, `Testably.Abstractions*`, threading analyzer) **auto-merge** once
the `Build & test (.NET)` check passes — branch protection on `main` requires that context;
everything else is manual review (ersatztv is prod-bearing). Range-pinned packages (e.g. EF
Core `[9.0.x,10)`) are respected — no v10 jump. PR volume is throttled (`prConcurrentLimit`
everything else is manual review (ersatztv is prod-bearing). A version range in
`Directory.Packages.props` does **not** hold Renovate back: the Dependency Dashboard (#22,
2026-10-27) offers EF Core `[9.0.12,10)` → v10 and `MediatR [12.5.0]` → `[14.2.0]`. A package that
must not move gets an `allowedVersions` packageRule in `renovate.json` (today: `SixLabors.ImageSharp`,
`security.imagesharp-3x-tiff-decoder-disabled`). PR volume is throttled (`prConcurrentLimit`
5 + `config:recommended`'s `prHourlyLimit` 2); tick a dashboard checkbox or raise the limits
to drain faster. `workflow_dispatch` defaults to a safe **dry run**. Cross-repo rollout
tracked in server-management#484. (server-management#484)
+1 -1
View File
@@ -184,7 +184,7 @@ the link for rationale. Superseded/retired history lives in `archive/`. Regenera
| `security.corp-same-origin` | `SecurityHeadersMiddleware` sends `Cross-Origin-Resource-Policy: same-origin` on every response including `/docs`/`/openapi`, blocking cross-origin `no-cors` embedding without affecting allowed CORS-mode fetches or server-side Jellyfin `/iptv/*` requests. | 2026-07-13 | [link](records/security/corp-same-origin.md) |
| `security.csp-permissions-policy` | `SecurityHeadersMiddleware` sends an enforcing (not report-only) `Content-Security-Policy` (no `unsafe-inline`/`unsafe-eval`; the one inline theme-bootstrap script allow-listed by hash) and a deny-all `Permissions-Policy` on the SPA/`/api`/`/artwork`/`/iptv`; `/docs` and `/openapi` keep only the baseline headers, excluded from CSP because Scalar needs inline bootstrap. | 2026-07-12 | [link](records/security/csp-permissions-policy.md) |
| `security.fail-closed-api-auth` | Every mutating `/api` request requires `X-Api-Key` (no open mode); reads are gated by `Api:RequireKeyForReads` (default true) OR `[RequiresApiKey]` on sensitive controllers; CORS is an exact-origin allowlist (`ApiCors`); `ForwardedHeaders` trust stays configurable but defaults to trust-all-with-warning. | 2026-07-12 | [link](records/security/fail-closed-api-auth.md) |
| `security.imagesharp-3x-tiff-decoder-disabled` | SixLabors.ImageSharp stays on 3.1.12 until a Six Labors license key is provisioned for CI and the image build. Every ImageSharp decode (Identify and Load, remote and local) passes `ImageSharpDecoding.Options(...)`, whose configuration registers every format `Configuration.Default` does EXCEPT TIFF; no code calls `IccProfile.Entries`, a TIFF encoder or `HistogramEqualization`. Revisit trigger: a provisioned key — then move to the patched line and re-measure the #511 decoder behaviours. | 2026-10-27 | [link](records/security/imagesharp-3x-tiff-decoder-disabled.md) |
| `security.imagesharp-3x-tiff-decoder-disabled` | SixLabors.ImageSharp stays on 3.1.12 until a Six Labors license key is provisioned for CI and the image build. Every ImageSharp decode (Identify and Load, remote and local) passes `ImageSharpDecoding.Options(...)`, whose configuration registers every format `Configuration.Default` does EXCEPT TIFF; no code references the TIFF codec, ICC profile types or the histogram-equalization processors. `ImageSharpSurfaceTests` enforces both over the IL of every production assembly that can reach ImageSharp. Revisit trigger: a provisioned key — then move to the patched line, drop the Renovate hold and re-measure the #511 decoder behaviours. | 2026-10-27 | [link](records/security/imagesharp-3x-tiff-decoder-disabled.md) |
| `security.iptv-access-token-transport` | The `/iptv` `?access_token=` value is percent-encoded (`Uri.EscapeDataString`) everywhere it is interpolated into an M3U/HLS/XMLTV URL (XMLTV additionally XML-escapes the encoded value), so a structural character can't malform the manifest or guide; Serilog logs a scrubbed request path (`access_token` → `***` via `IncludeQueryInRequestPath = false` + a `RequestPathScrubbed` enricher), so a 5xx/Debug `/iptv` request never writes the token; and every dynamic token-bearing `/iptv` manifest (`channels.m3u`, `xmltv.xml`, the HLS multi-variant/media playlists) returns `Cache-Control: private, no-store`. | 2026-07-23 | [link](records/security/iptv-access-token-transport.md) |
| `security.iptv-browser-token` | Under a JWT-enabled deployment (`JWT:IssuerSigningKey` set), the browser SPA obtains a short-lived, globally-scoped `/iptv/*` access token from an authenticated `GET /api/v1/auth/iptv-token` and appends it as `?access_token=`; the endpoint answers 204 when JWT is disabled (nothing to mint). Lifetime defaults to 60 min, configurable via `JWT:BrowserTokenLifetimeMinutes`. | 2026-07-22 | [link](records/security/iptv-browser-token.md) |
| `security.session-auth-dual-credential` | `ApiAuthorizationFilter` accepts a request when a valid `X-Api-Key` matches OR the principal is an authenticated session (cookie `ctv-session`, `HttpOnly`/`SameSite=Lax`); session-authenticated mutations require the presence-only `X-CSRF` header or are rejected 403. This narrows the OIDC-inert sub-claim of `security.blazor-removal-auth-posture` (#206) — the rest of that record's auth-surface enumeration still holds. | 2026-07-12 | [link](records/security/session-auth-dual-credential.md) |
@@ -5,8 +5,8 @@ status: active
since: '2026-10-27'
supersedes: none
superseded-by: none
rule: 'SixLabors.ImageSharp stays on 3.1.12 until a Six Labors license key is provisioned for CI and the image build. Every ImageSharp decode (Identify and Load, remote and local) passes `ImageSharpDecoding.Options(...)`, whose configuration registers every format `Configuration.Default` does EXCEPT TIFF; no code calls `IccProfile.Entries`, a TIFF encoder or `HistogramEqualization`. Revisit trigger: a provisioned key — then move to the patched line and re-measure the #511 decoder behaviours.'
signals: 'ImageSharp upgrade, ImageSharp 4, Six Labors license key, SixLaborsLicenseKey, No Six Labors license found, Release build fails, Renovate ImageSharp PR, vulnerable ImageSharp, TIFF decoder disabled, UnknownImageFormatException, ICC profile, dotnet list package --vulnerable, NuGetAuditSuppress · paths: `ErsatzTV.Infrastructure/Images/ImageSharpDecoding.cs`, `RemoteImageValidator.DecodeAndValidate`, `ImageElementBase.LoadImage`, `ImageSharpDecodingTests`, `Directory.Packages.props` · issues: #929, #923, #511, #525, #924'
rule: 'SixLabors.ImageSharp stays on 3.1.12 until a Six Labors license key is provisioned for CI and the image build. Every ImageSharp decode (Identify and Load, remote and local) passes `ImageSharpDecoding.Options(...)`, whose configuration registers every format `Configuration.Default` does EXCEPT TIFF; no code references the TIFF codec, ICC profile types or the histogram-equalization processors. `ImageSharpSurfaceTests` enforces both over the IL of every production assembly that can reach ImageSharp. Revisit trigger: a provisioned key — then move to the patched line, drop the Renovate hold and re-measure the #511 decoder behaviours.'
signals: 'ImageSharp upgrade, ImageSharp 4, Six Labors license key, SixLaborsLicenseKey, No Six Labors license found, Release build fails, Renovate ImageSharp PR, vulnerable ImageSharp, TIFF decoder disabled, UnknownImageFormatException, ICC profile, dotnet list package --vulnerable, NuGetAuditSuppress · paths: `ErsatzTV.Infrastructure/Images/ImageSharpDecoding.cs`, `RemoteImageValidator.DecodeAndValidate`, `ImageElementBase.LoadImage`, `ImageSharpDecodingTests`, `ErsatzTV.Architecture.Tests/ImageSharpSurfaceTests.cs`, `Directory.Packages.props`, `renovate.json` · issues: #929, #923, #511, #525, #924'
mechanics: '`ImageSharpDecoding.Configuration` is a fresh `Configuration` built from the format modules, NOT `Configuration.Default.Clone()` — Clone is shallow, so removing a detector from it would remove it from Default for the whole process'
---
@@ -41,21 +41,43 @@ render time, logo URLs saved via #525, uploaded artwork) and the local-path load
clamped to PNG/JPEG/GIF/WebP by `ImageContentTypes`, so the only input this rejects is a remote-URL
or on-disk TIFF graphic, which now disables its element the same way an unreachable URL does.
**What the tests prove, and what they do not.** `ImageSharpDecodingTests` derives the expected
format list from `Configuration.Default` minus TIFF, so a package bump that adds a format reddens
rather than silently narrowing the decoder; a header-only TIFF declaring 30000x30000 pins the
Identify pass (a TIFF-aware Identify would report the size and fail the pixel budget instead). The
watermark fixture pins the local and remote element paths with PNG twins. The Load pass's own
configuration is MASKED: Identify rejects first, so reverting only the Load options leaves the
suite green (measured 2026-10-27). It is kept so both passes agree on the format set.
**What holds the bounds.** Every bound in the table is a property of how ErsatzTV calls
ImageSharp, so each is enforced over the compiled code rather than left to review.
`ImageSharpSurfaceTests` (`ErsatzTV.Architecture.Tests`) reads the IL of every non-test project in
`ErsatzTV.sln` whose project-reference closure contains the ImageSharp package — the population is
derived from the solution and csproj graph, and a project that joins it without being referenced
by the test project fails the fixture. It rejects three shapes: an ImageSharp decode entry point
(`Load*`, `Identify*`, `DetectFormat*`, `Decode*`) called without `DecoderOptions`, a
`DecoderOptions` constructed outside `ImageSharpDecoding`, and any reference into the
`Formats.Tiff`, `Metadata.Profiles.Icc` or `Processing.Processors.Normalization` namespaces, the
`HistogramEqualizationExtensions` type, or an ImageSharp member whose name contains `Tiff` or
`HistogramEqualization` (`ImageExtensions.SaveAsTiff` carries no TIFF type in its signature). Each
check also asserts it sees something known to exist — the infrastructure and host assemblies in
the population, the two decode sites, the one `DecoderOptions` construction, the `Resize` call
inside a lambda — so an empty scan cannot pass.
`ImageSharpDecodingTests` derives the expected format list from `Configuration.Default` minus
TIFF, so a package bump that adds a format reddens rather than silently narrowing the decoder; a
header-only TIFF declaring 30000x30000 exercises the Identify pass (a TIFF-aware Identify would
report the size and fail the pixel budget instead). The watermark fixture exercises the local and
remote element paths with PNG twins. Identify rejects a TIFF before Load runs, so these
behavioural fixtures exercise the Identify pass; that the Load pass uses the same options is held
by `ImageSharpSurfaceTests`. One shape is outside every check: a `Save(path)` that picks its
encoder from the file extension. There is no ImageSharp encode in production code (2026-10-27).
**The scheduled scan stays red on ImageSharp until the revisit.** `NuGetAuditSuppress` silences the
NU1902/NU1903 restore warnings but `dotnet list package --vulnerable` ignores it (measured
2026-10-27: a suppressed GHSA still listed), so suppression would hide the advisory from build logs
without making `dependency-scan.yml` green. None is added; the advisories stay visible.
**Renovate is held too.** A `renovate.json` packageRule limits `SixLabors.ImageSharp` to `3.1.x`
for ordinary update PRs. A version range in `Directory.Packages.props` would not: the Dependency
Dashboard (#22, 2026-10-27) offers `MediatR [12.5.0]` → `[14.2.0]` and EF Core `[9.0.12,10)` → v10
for the existing range pins. Whether the rule also holds Renovate's OSV security PR (#923) is
unmeasured — Renovate reads its config from `main`, so the rule cannot be probed before merge.
**On revisit.** With a key provisioned (a CI secret passed as `SixLaborsLicenseKey`, and the same
for the Docker build), move to the patched line. The #511 measurements in
for the Docker build), delete the Renovate rule and move to the patched line. The #511 measurements in
`ffmpeg.remote-image-fetcher-bounded` (APNG `FrameMetadataCollection.Count == 0`, the default
Identify throwing on APNGs, `MaxFrames` honoured per format) were taken on 3.1.12 and must be
re-measured on the new version before the workaround comments are trusted. Whether TIFF comes back
+6 -1
View File
@@ -67,7 +67,12 @@ with have sat in these gaps. Treat anything not named here as unassessed, not as
the check proves each job was classified, never that the classification is CORRECT; and an
`inline` assertion cannot be proven to work without running the job, so those rows carry
`Proof: NONE` honestly instead of borrowing a neighbouring file's proof.
2. **C# and TypeScript guards** — seven files, none with a row:
2. **C# and TypeScript guards** — none with a row. The list is hand-written, so it is a floor and
not the population:
`ErsatzTV.Architecture.Tests/ImageSharpSurfaceTests.cs` (ersatztv#929 — derives the assemblies
that can reach ImageSharp from `ErsatzTV.sln` and the csproj graph and rejects decode, TIFF, ICC
and histogram-equalization call shapes in their IL), its siblings
`ErsatzTV.Architecture.Tests/LayeringTests.cs` and `…/ProviderStaticsWiringTests.cs`,
`ErsatzTV.Tests/Integration/SearchIndexMutationCoverageTests.cs` (ersatztv#824 — derives the
`ISearchIndex` population from the declaring assembly and asserts set equality against a
hand-written covered set),
+6 -1
View File
@@ -41,7 +41,7 @@
"groupName": "Microsoft.AspNetCore"
},
{
"description": "Group Entity Framework Core + providers (held to v9 by the range pins in Directory.Packages.props)",
"description": "Group Entity Framework Core + providers. The range pins in Directory.Packages.props hold the build to v9 but do not stop Renovate proposing v10 (docs/ci-cd.md, Renovate)",
"matchPackageNames": [
"/^Microsoft\\.EntityFrameworkCore/",
"/^EFCore\\.BulkExtensions/",
@@ -49,6 +49,11 @@
],
"groupName": "Entity Framework Core"
},
{
"description": "Hold SixLabors.ImageSharp on 3.1.x: every patched release fails a Release build without a Six Labors license key. Delete this rule when the key is provisioned (security.imagesharp-3x-tiff-decoder-disabled, ersatztv#929)",
"matchPackageNames": ["SixLabors.ImageSharp"],
"allowedVersions": "/^3\\.1\\./"
},
{
"description": "Group the Serilog family into one PR",
"matchPackageNames": ["/^Serilog/"],