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.