From 1ceb71b9a22b1e8a309ebba050aa08921ee0564d Mon Sep 17 00:00:00 2001 From: Anatolii Hrynchuk Date: Tue, 4 Aug 2026 21:01:25 +0300 Subject: [PATCH 1/2] fix: correct notification template filter links Use explicit Razor interpolation for create and edit URLs and cover the rendered link syntax with a regression test. Ref: IT-1039 --- .../AdminTemplatesIndexViewTests.cs | 32 +++++++++++++++++++ .../UnitTest1.cs | 10 ------ .../Views/AdminTemplates/Index.cshtml | 4 +-- docs/itemtracker-outbox-email-consumer.md | 6 ++++ 4 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 HrynCo.NotificationService.Web.IntegrationTests/AdminTemplatesIndexViewTests.cs delete mode 100644 HrynCo.NotificationService.Web.IntegrationTests/UnitTest1.cs diff --git a/HrynCo.NotificationService.Web.IntegrationTests/AdminTemplatesIndexViewTests.cs b/HrynCo.NotificationService.Web.IntegrationTests/AdminTemplatesIndexViewTests.cs new file mode 100644 index 0000000..65f2ce2 --- /dev/null +++ b/HrynCo.NotificationService.Web.IntegrationTests/AdminTemplatesIndexViewTests.cs @@ -0,0 +1,32 @@ +namespace HrynCo.NotificationService.Web.IntegrationTests; + +public sealed class AdminTemplatesIndexViewTests +{ + [Fact] + public void CreateAndEditLinks_UseExplicitFilterQueryInterpolation() + { + string view = File.ReadAllText(FindIndexView()); + + Assert.Contains("/admin/templates/create@(filterQuery)", view); + Assert.Contains("@t.LanguageCode@(filterQuery)", view); + Assert.DoesNotContain("create@filterQuery", view); + Assert.DoesNotContain("LanguageCode@filterQuery", view); + } + + private static string FindIndexView() + { + DirectoryInfo? directory = new(AppContext.BaseDirectory); + while (directory is not null && !File.Exists(Path.Combine(directory.FullName, "HrynCo.NotificationService.slnx"))) + { + directory = directory.Parent; + } + + Assert.NotNull(directory); + return Path.Combine( + directory.FullName, + "HrynCo.NotificationService.Web", + "Views", + "AdminTemplates", + "Index.cshtml"); + } +} diff --git a/HrynCo.NotificationService.Web.IntegrationTests/UnitTest1.cs b/HrynCo.NotificationService.Web.IntegrationTests/UnitTest1.cs deleted file mode 100644 index 3e48a76..0000000 --- a/HrynCo.NotificationService.Web.IntegrationTests/UnitTest1.cs +++ /dev/null @@ -1,10 +0,0 @@ -namespace HrynCo.NotificationService.Web.IntegrationTests; - -public class UnitTest1 -{ - [Fact] - public void Test1() - { - - } -} diff --git a/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml b/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml index 20907cb..69cecfc 100644 --- a/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml +++ b/HrynCo.NotificationService.Web/Views/AdminTemplates/Index.cshtml @@ -14,7 +14,7 @@ @@ -146,7 +146,7 @@ else @t.LanguageCode @t.Subject - Edit diff --git a/docs/itemtracker-outbox-email-consumer.md b/docs/itemtracker-outbox-email-consumer.md index 2db12f1..68df889 100644 --- a/docs/itemtracker-outbox-email-consumer.md +++ b/docs/itemtracker-outbox-email-consumer.md @@ -112,4 +112,10 @@ version. Before enabling the Worker, verify that the target service has an active SMTP channel and exact-language templates for every queued ItemTracker template key. Inspect any delayed ItemTracker backlog for expired password-reset, verification, or invitation messages before draining it. +The template administration list preserves optional Service Name and Key filters when an +administrator opens the create or edit screen. These links use explicit Razor expression +boundaries so the filter query is appended as query parameters rather than rendered as a +literal `@filterQuery` path segment. With no active filters, the create route is exactly +`/admin/templates/create`. + For the smoke test, use an owned test account and a non-sensitive notification template. Record the Outbox `Id` and `CorrelationId`, trigger only one message, follow those identifiers through Outbox publication and Notification Service logs, and confirm receipt with the configured SMTP provider. Do not copy payloads, tokens, credentials, full recipient addresses, or rendered URLs into tickets or logs. From 50033a5bd4090909aeea2d79284276c3ca788f47 Mon Sep 17 00:00:00 2001 From: Anatolii Hrynchuk Date: Wed, 19 Aug 2026 23:04:15 +0300 Subject: [PATCH 2/2] fix: escape html notification variables Encode template variables in HTML bodies while preserving text and subjects. Ref: IT-1115 --- .../EmailTemplateRenderingServiceTests.cs | 21 +++++++++++++++++++ .../EmailTemplateRenderingService.cs | 18 ++++++++++++++-- README.md | 4 ++++ 3 files changed, 41 insertions(+), 2 deletions(-) diff --git a/HrynCo.NotificationService.Services.Tests/EmailProcessing/EmailTemplateRenderingServiceTests.cs b/HrynCo.NotificationService.Services.Tests/EmailProcessing/EmailTemplateRenderingServiceTests.cs index 5fa3cc8..cca8669 100644 --- a/HrynCo.NotificationService.Services.Tests/EmailProcessing/EmailTemplateRenderingServiceTests.cs +++ b/HrynCo.NotificationService.Services.Tests/EmailProcessing/EmailTemplateRenderingServiceTests.cs @@ -40,6 +40,27 @@ public sealed class EmailTemplateRenderingServiceTests Assert.Equal("Required template variables are missing: VerificationUrl.", exception.Message); } + [Fact] + public void Render_HtmlEncodesVariablesWithoutChangingSubjectOrTextBody() + { + EmailTemplate template = CreateTemplate(); + SendEmailMessageData data = CreateData(new Dictionary + { + ["AppName"] = "Invemory ", + ["VerificationUrl"] = "https://example.invalid/verify?next=\" onclick=\"alert('xss')" + }); + + RenderedEmail result = _service.Render(template, data); + + Assert.Equal("Verify Invemory ", result.Subject); + Assert.Equal( + "Verify", + result.HtmlBody); + Assert.Equal( + "Verify at https://example.invalid/verify?next=\" onclick=\"alert('xss')", + result.TextBody); + } + private static EmailTemplate CreateTemplate() => new() { ServiceName = "StoreMate-Prod", diff --git a/HrynCo.NotificationService.Worker.Services/EmailProcessing/EmailTemplateRenderingService.cs b/HrynCo.NotificationService.Worker.Services/EmailProcessing/EmailTemplateRenderingService.cs index af778ca..2c46768 100644 --- a/HrynCo.NotificationService.Worker.Services/EmailProcessing/EmailTemplateRenderingService.cs +++ b/HrynCo.NotificationService.Worker.Services/EmailProcessing/EmailTemplateRenderingService.cs @@ -1,5 +1,6 @@ namespace HrynCo.NotificationService.Worker.Services.EmailProcessing; +using System.Net; using System.Text; using HrynCo.NotificationService.Contracts.Messages; using HrynCo.NotificationService.DAL.Abstract.Templates; @@ -22,15 +23,28 @@ internal sealed class EmailTemplateRenderingService : IEmailTemplateRenderingSer return new RenderedEmail( Interpolate(template.Subject, data.Variables), - Interpolate(template.HtmlBody, data.Variables), + InterpolateHtml(template.HtmlBody, data.Variables), Interpolate(template.TextBody, data.Variables)); } + private static string InterpolateHtml(string text, IReadOnlyDictionary variables) + { + return Interpolate(text, variables, WebUtility.HtmlEncode); + } + private static string Interpolate(string text, IReadOnlyDictionary variables) + { + return Interpolate(text, variables, static value => value); + } + + private static string Interpolate( + string text, + IReadOnlyDictionary variables, + Func encodeValue) { var sb = new StringBuilder(text); foreach (var (key, value) in variables) - sb.Replace($"{{{{{key}}}}}", value); + sb.Replace($"{{{{{key}}}}}", encodeValue(value)); return sb.ToString(); } } diff --git a/README.md b/README.md index 3f288d5..0ea3260 100644 --- a/README.md +++ b/README.md @@ -112,3 +112,7 @@ flowchart TD M -->|retries exhausted| N[Publish terminal failure result] N --> O[Nack original delivery without requeue] ``` + +Template variables are treated as plain text. The worker HTML-encodes every variable while +rendering `HtmlBody`; subject and plain-text body interpolation preserve the original value. +Templates must express markup in `body.html` instead of supplying HTML through variables.