From bf15677dc638a79be3250472d0c2c738c09462b3 Mon Sep 17 00:00:00 2001 From: Timothy Date: Fri, 17 Jul 2026 19:04:42 +0200 Subject: [PATCH] fix(376): XML-escape access_token value in XMLTV guide output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `GetChannelGuideHandler` interpolated `request.AccessToken` (HTTP-request- derived, from `?access_token=`) raw into the pre-built XMLTV cache fragments. A token containing `&`, `<`, `>`, or `"` would emit invalid XML and malform the entire guide. Escape it with `SecurityElement.Escape`, consistent with how #340 escaped `{RequestBase}`. The M3U path (`ChannelPlaylist.ToM3U`) also interpolates the token but M3U is not XML, so escaping there is neither needed nor correct — left unchanged. Regression test `Guide_xml_escapes_access_token` drives the real handler with a token containing all four XML-special chars and asserts the output is escaped (sibling to the #340 `Guide_xml_escapes_advertised_base_url` test). Verified non-vacuous: it fails with the escape reverted. fixes #376 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Queries/GetChannelGuideHandler.cs | 5 ++- .../Iptv/ChannelGuideGoldenTests.cs | 41 +++++++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs index bbebfd43d..62be03c36 100644 --- a/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs +++ b/ErsatzTV.Application/Channels/Queries/GetChannelGuideHandler.cs @@ -60,7 +60,10 @@ public partial class GetChannelGuideHandler( var accessTokenUri = $"?v={mtime}"; if (!string.IsNullOrWhiteSpace(request.AccessToken)) { - accessTokenUri += $"&access_token={request.AccessToken}"; + // The token value is HTTP-request-derived and interpolated raw into the pre-built XMLTV + // cache fragments, so it must be XML-escaped like {RequestBase} above — a token containing + // '&', '<', '>', or '"' would otherwise malform the whole guide. Opaque tokens are a no-op. + accessTokenUri += $"&access_token={SecurityElement.Escape(request.AccessToken)}"; } string channelsFragment = await ReadAllTextShared(channelsFile, cancellationToken); diff --git a/ErsatzTV.Core.Tests/Iptv/ChannelGuideGoldenTests.cs b/ErsatzTV.Core.Tests/Iptv/ChannelGuideGoldenTests.cs index a78591793..966c763b1 100644 --- a/ErsatzTV.Core.Tests/Iptv/ChannelGuideGoldenTests.cs +++ b/ErsatzTV.Core.Tests/Iptv/ChannelGuideGoldenTests.cs @@ -187,6 +187,47 @@ public class ChannelGuideGoldenTests xml.ShouldNotContain("a&b"); } + // The access-token value is HTTP-request-derived (?access_token=) and interpolated raw into the + // {AccessTokenUri} placeholder, so a token containing XML-special chars must be escaped too — + // otherwise it malforms the whole guide, exactly like the {RequestBase} case above. (Finding #376.) + [Test] + public async Task Guide_xml_escapes_access_token() + { + MockFileSystem fileSystem = BuildCacheFileSystem(); + var localFileSystem = Substitute.For(); + localFileSystem + .ListFiles(FileSystemLayout.ChannelGuideCacheFolder) + .Returns(new[] + { + FragmentPath(fileSystem, "channels.xml"), + FragmentPath(fileSystem, "2.xml") + }); + + var configElementRepository = Substitute.For(); + configElementRepository + .GetValue(Arg.Any(), Arg.Any()) + .Returns(Option.None); + + var handler = new GetChannelGuideHandler( + _dbContextFactory, + new RecyclableMemoryStreamManager(), + fileSystem, + localFileSystem, + configElementRepository); + + Either result = await handler.Handle( + new GetChannelGuide(Scheme, Host, BaseUrl: "", AccessToken: "tok&<>\""), + CancellationToken.None); + + string xml = result.Match( + Right: guide => guide.ToXml(), + Left: error => throw new AssertionException($"Handler returned error: {error.Value}")); + + // Every XML-special char in the token must be escaped; the raw token must never reach the output. + xml.ShouldContain("access_token=tok&<>""); + xml.ShouldNotContain("access_token=tok&<"); + } + // --- harness --- private async Task Verify(string goldenName, GetChannelGuide request)