Skip to content

Commit 79da4ac

Browse files
committed
test(email): add XSS + plain-text verbatim guards for RIExchange HTML renderer
Add two tests missing from the #296 wave: - TestRenderRIExchangePendingApprovalEmailHTML_EscapesHostilePayload: feeds <script>alert('xss')</script> into RequestedByName, CancellationWindowNote, and Skipped.Reason and asserts the literal tag is absent from the rendered HTML (html/template auto-escaping). - TestRenderRIExchangePendingApprovalEmail_PlainTextVerbatim: feeds O'Brien & Co and a plus-addressed email into the plain-text renderer and asserts no &#39; / &amp; entity encoding appears (text/template emits values verbatim). Also replace em-dash with double-hyphen in the HTML template comment (style rule).
1 parent 0e46368 commit 79da4ac

2 files changed

Lines changed: 57 additions & 1 deletion

File tree

‎internal/email/template_renderers_test.go‎

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -734,3 +734,59 @@ func TestRenderRIExchangePendingApprovalEmailHTML_SkippedBlock(t *testing.T) {
734734
assert.Contains(t, html, "no matching target available")
735735
assert.Contains(t, html, "Skipped")
736736
}
737+
738+
// Issue #296 / XSS guard: html/template must escape hostile payloads in the
739+
// RIExchange HTML renderer. Tests RequestedByName, CancellationWindowNote, and
740+
// Skipped.Reason -- the three free-text fields most likely to carry attacker
741+
// input. Mirrors the analogous sub-test in TestPlainTextTemplates_NoHTMLEscaping.
742+
func TestRenderRIExchangePendingApprovalEmailHTML_EscapesHostilePayload(t *testing.T) {
743+
const xssPayload = `<script>alert('xss')</script>`
744+
data := RIExchangeNotificationData{
745+
DashboardURL: "https://dashboard.example.com",
746+
TotalPayment: "0.00",
747+
RequestedByName: xssPayload,
748+
RequestedByEmail: "attacker@evil.example",
749+
CancellationWindowNote: xssPayload,
750+
Exchanges: []RIExchangeItem{},
751+
Skipped: []SkippedExchange{{
752+
SourceRIID: "ri-skip-xss",
753+
SourceInstanceType: "m5.large",
754+
Reason: xssPayload,
755+
}},
756+
}
757+
758+
html, err := RenderRIExchangePendingApprovalEmailHTML(data)
759+
require.NoError(t, err)
760+
761+
// The literal script tag must not appear verbatim in the HTML output.
762+
assert.NotContains(t, html, xssPayload, "html/template must escape <script> tags in RequestedByName/CancellationWindowNote/Reason")
763+
// The content must still appear (escaped), confirming the field is rendered at all.
764+
assert.Contains(t, html, "alert(", "escaped payload content should still appear in output")
765+
}
766+
767+
// Issue #296 / plain-text guard: text/template must emit special characters
768+
// verbatim in the RIExchange plain-text renderer (no HTML entity encoding).
769+
func TestRenderRIExchangePendingApprovalEmail_PlainTextVerbatim(t *testing.T) {
770+
name := "O'Brien & Co"
771+
emailAddr := "o.brien+tag@acme.com"
772+
data := RIExchangeNotificationData{
773+
DashboardURL: "https://dashboard.example.com",
774+
TotalPayment: "0.00",
775+
RequestedByName: name,
776+
RequestedByEmail: emailAddr,
777+
Exchanges: []RIExchangeItem{{
778+
RecordID: "rec-plain", ApprovalToken: "tok-plain",
779+
SourceRIID: "ri-ccc", SourceInstanceType: "r5.large",
780+
TargetInstanceType: "r6i.large", TargetCount: 1,
781+
PaymentDue: "0.00", UtilizationPct: 55.0,
782+
}},
783+
}
784+
785+
body, err := RenderRIExchangePendingApprovalEmail(data)
786+
require.NoError(t, err)
787+
788+
assert.Contains(t, body, name, "apostrophe+ampersand in RequestedByName must be verbatim in plain-text")
789+
assert.Contains(t, body, emailAddr, "plus-addressed email must be verbatim in plain-text")
790+
assert.NotContains(t, body, "&#39;", "plain-text must not HTML-encode apostrophe")
791+
assert.NotContains(t, body, "&amp;", "plain-text must not HTML-encode ampersand")
792+
}

‎internal/email/templates.go‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -321,7 +321,7 @@ This is an automated message from CUDly. Do not share these links.
321321
// riExchangePendingApprovalHTMLTemplate renders the same approval request as
322322
// the plain-text template above with inline-styled approve/reject CTAs and an
323323
// exchange summary table. CSS classes are NOT honoured by most email clients
324-
// (Outlook, mobile Gmail) — every visual rule lives in inline style=""
324+
// (Outlook, mobile Gmail) -- every visual rule lives in inline style=""
325325
// attributes. Mirrors purchaseApprovalRequestHTMLTemplate. Issue #296.
326326
const riExchangePendingApprovalHTMLTemplate = `<!DOCTYPE html>
327327
<html><head><meta charset="UTF-8"><title>CUDly - RI Exchange Approval Required</title></head>

0 commit comments

Comments
 (0)