Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 8 additions & 8 deletions coderd/notifications/dispatch/smtp/html.gotmpl
Original file line number Diff line number Diff line change
Expand Up @@ -3,31 +3,31 @@
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{{ .Labels._subject }}</title>
<title>{{ .Labels._subject | html }}</title>
</head>
<body style="margin: 0; padding: 0; font-family: -apple-system, system-ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; color: #020617; background: #f8fafc;">
<div style="max-width: 600px; margin: 20px auto; padding: 60px; border: 1px solid #e2e8f0; border-radius: 8px; background-color: #fff; text-align: left; font-size: 14px; line-height: 1.5;">
<div style="text-align: center;">
<img src="{{ logo_url | html }}" alt="{{ app_name | html }} Logo" style="height: 40px;" />
</div>
<h1 style="text-align: center; font-size: 24px; font-weight: 400; margin: 8px 0 32px; line-height: 1.5;">
{{ .Labels._subject }}
{{ .Labels._subject | html }}
</h1>
<div style="line-height: 1.5;">
<p>Hi {{ .UserName }},</p>
<p>Hi {{ .UserName | html }},</p>
{{ .Labels._body }}
</div>
<div style="text-align: center; margin-top: 32px;">
{{ range $action := .Actions }}
<a href="{{ $action.URL }}" style="display: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fafc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
{{ $action.Label }}
<a href="{{ $action.URL | html }}" style="display: inline-block; padding: 13px 24px; background-color: #020617; color: #f8fafc; text-decoration: none; border-radius: 8px; margin: 0 4px;">
{{ $action.Label | html }}
</a>
{{ end }}
</div>
<div style="border-top: 1px solid #e2e8f0; color: #475569; font-size: 12px; margin-top: 64px; padding-top: 24px; line-height: 1.6;">
<p>&copy;&nbsp;{{ current_year }}&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a href="{{ base_url }}" style="color: #2563eb; text-decoration: none;">{{ base_url }}</a></p>
<p><a href="{{ base_url }}/settings/notifications" style="color: #2563eb; text-decoration: none;">Click here to manage your notification settings</a></p>
<p><a href="{{ base_url }}/settings/notifications?disabled={{ .NotificationTemplateID }}" style="color: #2563eb; text-decoration: none;">Stop receiving emails like this</a></p>
<p>&copy;&nbsp;{{ current_year | html }}&nbsp;Coder. All rights reserved&nbsp;-&nbsp;<a href="{{ base_url | html }}" style="color: #2563eb; text-decoration: none;">{{ base_url | html }}</a></p>
<p><a href="{{ base_url | html }}/settings/notifications" style="color: #2563eb; text-decoration: none;">Click here to manage your notification settings</a></p>
<p><a href="{{ base_url | html }}/settings/notifications?disabled={{ .NotificationTemplateID | html }}" style="color: #2563eb; text-decoration: none;">Stop receiving emails like this</a></p>
</div>
</div>
</body>
Expand Down
160 changes: 154 additions & 6 deletions coderd/notifications/dispatch/smtp_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,100 @@ import (

"github.com/coder/coder/v2/coderd/notifications/render"
"github.com/coder/coder/v2/coderd/notifications/types"
markdown "github.com/coder/coder/v2/coderd/render"
)

// Benign values, so a test measures only what its own payload injected.
func templateHelpers() map[string]any {
return map[string]any{
"base_url": func() string { return "https://coder.example.com" },
"current_year": func() string { return "2026" },
"logo_url": func() string { return "https://coder.example.com/logo.png" },
"app_name": func() string { return "Coder" },
}
}

func TestSMTPHTMLTemplateEscapesUntrustedValues(t *testing.T) {
t.Parallel()

for _, tc := range []struct {
name string
title string
userName string
actions []types.TemplateAction
injected string
}{
{
name: "EntityEncodedAnchorInSubject",
title: `Template "&lt;a href="https://attacker.example/login"&gt;Re-authenticate now&lt;/a&gt;" deleted`,
userName: "Bobby",
injected: `<a href="https://attacker.example/login">Re-authenticate now</a>`,
},
{
name: "EntityEncodedImageInSubject",
title: `Workspace "&lt;img src=x onerror="alert(1)"&gt;" marked dormant`,
userName: "Bobby",
injected: `<img src=x onerror="alert(1)">`,
},
{
name: "RawHTMLInUserName",
title: "Account suspended",
userName: `Bobby <img src=x onerror="alert(1)">`,
injected: `<img src=x onerror="alert(1)">`,
},
{
name: "RawHTMLInActionLabel",
title: "Account suspended",
userName: "Bobby",
actions: []types.TemplateAction{
{Label: `<img src=x onerror="alert(1)">`, URL: "https://coder.example.com/"},
},
injected: `<img src=x onerror="alert(1)">`,
},
{
name: "RawHTMLInActionURL",
title: "Account suspended",
userName: "Bobby",
actions: []types.TemplateAction{
{Label: "Open Coder", URL: `https://coder.example.com/?x=<script>alert(1)</script>`},
},
injected: `<script>alert(1)</script>`,
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

// Decodes the entities, so the title arrives as live markup.
subject, err := markdown.PlaintextFromMarkdown(tc.title)
require.NoError(t, err)

// Actions are set as the template sees them. The enqueuer renders
// them into JSON first, which rejects a `"` of its own accord.
payload := types.MessagePayload{
NotificationTemplateID: "00000000-0000-0000-0000-000000000000",
UserName: tc.userName,
Actions: tc.actions,
Labels: map[string]string{
"_subject": subject,
"_body": "<p>Test body</p>",
},
}

got, err := render.GoTemplate(htmlTemplate, payload, templateHelpers())
require.NoError(t, err)

escaped := html.EscapeString(tc.injected)
require.NotEqual(t, tc.injected, escaped,
"case carries no HTML to escape, so it guards nothing")

require.NotContains(t, got, tc.injected,
"untrusted markup reached the rendered email: %s", got)
require.Contains(t, got, escaped,
"the value must still be displayed, entity encoded: %s", got)
})
}
}

func TestSMTPHTMLTemplateEscapesAppearanceHelpers(t *testing.T) {
t.Parallel()

Expand All @@ -28,12 +120,9 @@ func TestSMTPHTMLTemplateEscapesAppearanceHelpers(t *testing.T) {
"_body": "<p>Test body</p>",
},
}
helpers := map[string]any{
"base_url": func() string { return "https://coder.example.com" },
"current_year": func() string { return "2026" },
"logo_url": func() string { return logoURL },
"app_name": func() string { return appName },
}
helpers := templateHelpers()
helpers["logo_url"] = func() string { return logoURL }
helpers["app_name"] = func() string { return appName }

got, err := render.GoTemplate(htmlTemplate, payload, helpers)
require.NoError(t, err)
Expand All @@ -44,6 +133,65 @@ func TestSMTPHTMLTemplateEscapesAppearanceHelpers(t *testing.T) {
require.False(t, strings.Contains(got, logoURL), "raw logo URL must not be rendered")
}

// The template escapes every value it interpolates except _body, which is
// trusted rendered Markdown. The three values here cannot carry markup in
// production, so this test is the only thing that fails if their escaping is
// removed.
func TestSMTPHTMLTemplateEscapesTrustedValues(t *testing.T) {
t.Parallel()

const injected = `a"onclick=alert(1)`

for _, tc := range []struct {
name string
apply func(*types.MessagePayload, map[string]any)
}{
{
// net/url preserves a quote in the query and --access-url is
// validated for its scheme only, so an operator can land this.
name: "BaseURL",
apply: func(_ *types.MessagePayload, h map[string]any) {
h["base_url"] = func() string { return "https://coder.example.com/?q=" + injected }
},
},
{
name: "CurrentYear",
apply: func(_ *types.MessagePayload, h map[string]any) {
h["current_year"] = func() string { return injected }
},
},
{
name: "NotificationTemplateID",
apply: func(p *types.MessagePayload, _ map[string]any) {
p.NotificationTemplateID = injected
},
},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()

payload := types.MessagePayload{
NotificationTemplateID: "00000000-0000-0000-0000-000000000000",
UserName: "Test User",
Labels: map[string]string{
"_subject": "Test notification",
"_body": "<p>Test body</p>",
},
}
helpers := templateHelpers()
tc.apply(&payload, helpers)

got, err := render.GoTemplate(htmlTemplate, payload, helpers)
require.NoError(t, err)

require.NotContains(t, got, injected,
"raw value reached the rendered email: %s", got)
require.Contains(t, got, html.EscapeString(injected),
"the value must still be displayed, entity encoded: %s", got)
})
}
}

func TestValidateFromAddr(t *testing.T) {
t.Parallel()

Expand Down
4 changes: 3 additions & 1 deletion coderd/notifications/notifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,7 +253,9 @@ func (n *notifier) prepare(ctx context.Context, msg database.AcquireNotification
// Label and data values are user-controlled while the templates around them
// are not, so Markdown structure in a value is neutralized before it reaches
// the template. The dispatcher still receives the unescaped payload, because
// the webhook contract surfaces enqueued values verbatim.
// the webhook contract surfaces enqueued values verbatim. smtp/html.gotmpl
// escapes at its own sinks, which it must: PlaintextFromMarkdown strips this
// escaping back out of _subject.
escaped := payload.EscapedForMarkdown()

var title, body string
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-workspace' completed</title>
<title>Task &#39;my-workspace&#39; completed</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Task 'my-workspace' completed
Task &#39;my-workspace&#39; completed
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-workspace' failed</title>
<title>Task &#39;my-workspace&#39; failed</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Task 'my-workspace' failed
Task &#39;my-workspace&#39; failed
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-workspace' is idle</title>
<title>Task &#39;my-workspace&#39; is idle</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Task 'my-workspace' is idle
Task &#39;my-workspace&#39; is idle
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-task' is paused</title>
<title>Task &#39;my-task&#39; is paused</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Task 'my-task' is paused
Task &#39;my-task&#39; is paused
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-task' has resumed</title>
<title>Task &#39;my-task&#39; has resumed</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Task 'my-task' has resumed
Task &#39;my-task&#39; has resumed
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Task 'my-workspace' is working</title>
<title>Task &#39;my-workspace&#39; is working</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Task 'my-workspace' is working
Task &#39;my-workspace&#39; is working
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Template "Bobby's Template" deleted</title>
<title>Template &#34;Bobby&#39;s Template&#34; deleted</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -42,7 +42,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Template "Bobby's Template" deleted
Template &#34;Bobby&#39;s Template&#34; deleted
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>Template 'alpha' has been deprecated</title>
<title>Template &#39;alpha&#39; has been deprecated</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -50,7 +50,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
Template 'alpha' has been deprecated
Template &#39;alpha&#39; has been deprecated
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ Content-Type: text/html; charset=UTF-8
<meta charset=3D"UTF-8" />
<meta name=3D"viewport" content=3D"width=3Ddevice-width, initial-scale=
=3D1.0" />
<title>User account "bobby" activated</title>
<title>User account &#34;bobby&#34; activated</title>
</head>
<body style=3D"margin: 0; padding: 0; font-family: -apple-system, system-=
ui, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarel=
Expand All @@ -44,7 +44,7 @@ er Logo" style=3D"height: 40px;" />
</div>
<h1 style=3D"text-align: center; font-size: 24px; font-weight: 400; m=
argin: 8px 0 32px; line-height: 1.5;">
User account "bobby" activated
User account &#34;bobby&#34; activated
</h1>
<div style=3D"line-height: 1.5;">
<p>Hi Bobby,</p>
Expand Down
Loading
Loading