Files
ersatztv/ErsatzTV/Controllers/Api/Requests/UpdateWatermarkRequest.cs
T
timothyandClaude Opus 4.8 cf834d8b60
Build ErsatzTV Image / Docs update reminder (pull_request) Successful in 5s
Build ErsatzTV Image / Build & test (.NET) (pull_request) Successful in 8m33s
Build ErsatzTV Image / EF migration integrity (SQLite + MySql) (pull_request) Successful in 10m40s
Build ErsatzTV Image / Build & push image (amd64) (pull_request) Has been skipped
security(#283): sniff artwork content type from bytes, remove serve-side ?contentType= reflection
S4 stored-XSS + S9 upload-size DoS from the #197 cold API review.

The artwork path trusted client-supplied content types at both ends: upload
validated only the declared multipart Content-Type (never decoded the bytes),
and serving reflected a client `?contentType=` straight into the response
Content-Type on unauthenticated GET sinks (/iptv/logos, /artwork/watermarks).
Chain: upload <script> bytes as image/png -> GET ...?contentType=text/html
serves them as HTML in-origin. nosniff (#279) does not help because the server
explicitly declares text/html.

- Upload: derive the content type from the bytes via SkiaSharp SKCodec
  (header-only, no decode -> no decompression-bomb path); reject non-images 422.
  New ErsatzTV.Core/Images/ImageContentTypes as the single allow-list source.
  Dropped the untrusted declared Content-Type from the UploadArtwork command.
- Serve: removed the ?contentType= reflection structurally -- dropped ContentType
  from GetCachedImagePath and the [FromQuery] binding on GetImage/GetWatermark;
  the handler always sniffs the file, defaulting application/octet-stream.
  ArtworkContentTypeModel.UrlWithContentType is now the bare path; SPA previews
  no longer append the query.
- Defense-in-depth: channel-logo / watermark {path, contentType} DTOs run through
  ArtworkContentTypeModel.Sanitized(), blanking non-allow-listed types on write.
- S9: Kestrel MaxRequestBodySize from ETV_MAXIMUM_UPLOAD_MB rejects oversized
  bodies during read (controller file.Length check kept as friendly-error backstop).

Both serve sinks are IgnoreApi, so no OpenAPI change. Tests: byte-sniff accept/
reject, Sanitized() allow-list, Location no longer carries ?contentType=.
Docs: api-conventions §4a + decisions.md 2026-07-12.

Refs #283 #197 #66

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-12 00:07:55 +02:00

46 lines
1.2 KiB
C#

#nullable enable
using ErsatzTV.Application.Artworks;
using ErsatzTV.Application.Watermarks;
using ErsatzTV.Core.Domain;
using ErsatzTV.FFmpeg.State;
namespace ErsatzTV.Controllers.Api.Requests;
public record UpdateWatermarkRequest(
string Name,
ChannelWatermarkMode Mode,
ChannelWatermarkImageSource ImageSource,
string? Image,
string? ImageContentType,
WatermarkLocation Location,
WatermarkSize Size,
double Width,
double HorizontalMargin,
double VerticalMargin,
int FrequencyMinutes,
int DurationSeconds,
int Opacity,
string? OpacityExpression,
int ZIndex,
bool PlaceWithinSourceContent)
{
public UpdateWatermark ToCommand(int id) =>
new(
id,
Name,
new ArtworkContentTypeModel(Image ?? string.Empty, ImageContentType ?? string.Empty).Sanitized(),
Mode,
ImageSource,
Location,
Size,
Width,
HorizontalMargin,
VerticalMargin,
FrequencyMinutes,
DurationSeconds,
Opacity,
PlaceWithinSourceContent,
OpacityExpression ?? string.Empty,
ZIndex);
}