diff --git a/mails/record.go b/mails/record.go index cb1be4fe..25b50aeb 100644 --- a/mails/record.go +++ b/mails/record.go @@ -1,8 +1,10 @@ package mails import ( + "html" "html/template" "net/mail" + "slices" "github.com/pocketbase/pocketbase/core" "github.com/pocketbase/pocketbase/mails/templates" @@ -232,6 +234,13 @@ func SendRecordChangeEmail(app core.App, authRecord *core.Record, newEmail strin }) } +var nonescapeTypes = []string{ + core.FieldTypeAutodate, + core.FieldTypeDate, + core.FieldTypeBool, + core.FieldTypeNumber, +} + func resolveEmailTemplate( app core.App, authRecord *core.Record, @@ -258,7 +267,15 @@ func resolveEmailTemplate( fieldPlacehodler := "{RECORD:" + field.GetName() + "}" if _, ok := placeholders[fieldPlacehodler]; !ok { - placeholders[fieldPlacehodler] = authRecord.Get(field.GetName()) + val := authRecord.GetString(field.GetName()) + + // note: the escaping is not strictly necessary but for just in case + // the user decide to store and render the email as plain html + if !slices.Contains(nonescapeTypes, field.Type()) { + val = html.EscapeString(val) + } + + placeholders[fieldPlacehodler] = val } } diff --git a/mails/record_test.go b/mails/record_test.go index 1917493f..0f103dbb 100644 --- a/mails/record_test.go +++ b/mails/record_test.go @@ -1,6 +1,7 @@ package mails_test import ( + "html" "strings" "testing" @@ -16,6 +17,9 @@ func TestSendRecordAuthAlert(t *testing.T) { user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") + // to test that it is escaped + user.Set("name", "
"+user.GetString("name")+"
") + err := mails.SendRecordAuthAlert(testApp, user) if err != nil { t.Fatal(err) @@ -26,7 +30,7 @@ func TestSendRecordAuthAlert(t *testing.T) { } expectedParts := []string{ - user.GetString("name") + "{RECORD:tokenKey}", // public and private record placeholder checks + html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // public and private record placeholder checks "login to your " + testApp.Settings().Meta.AppName + " account from a new location", "If this was you", "If this wasn't you", @@ -46,6 +50,9 @@ func TestSendRecordPasswordReset(t *testing.T) { user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") + // to test that it is escaped + user.Set("name", ""+user.GetString("name")+"
") + err := mails.SendRecordPasswordReset(testApp, user) if err != nil { t.Fatal(err) @@ -56,7 +63,7 @@ func TestSendRecordPasswordReset(t *testing.T) { } expectedParts := []string{ - user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} + html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name} "http://localhost:8090/_/#/auth/confirm-password-reset/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", } for _, part := range expectedParts { @@ -74,6 +81,9 @@ func TestSendRecordVerification(t *testing.T) { user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") + // to test that it is escaped + user.Set("name", ""+user.GetString("name")+"
") + err := mails.SendRecordVerification(testApp, user) if err != nil { t.Fatal(err) @@ -84,7 +94,7 @@ func TestSendRecordVerification(t *testing.T) { } expectedParts := []string{ - user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} + html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name} "http://localhost:8090/_/#/auth/confirm-verification/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", } for _, part := range expectedParts { @@ -102,6 +112,9 @@ func TestSendRecordChangeEmail(t *testing.T) { user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") + // to test that it is escaped + user.Set("name", ""+user.GetString("name")+"
") + err := mails.SendRecordChangeEmail(testApp, user, "new_test@example.com") if err != nil { t.Fatal(err) @@ -112,7 +125,7 @@ func TestSendRecordChangeEmail(t *testing.T) { } expectedParts := []string{ - user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} + html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name} "http://localhost:8090/_/#/auth/confirm-email-change/eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.", } for _, part := range expectedParts { @@ -130,6 +143,9 @@ func TestSendRecordOTP(t *testing.T) { user, _ := testApp.FindFirstRecordByData("users", "email", "test@example.com") + // to test that it is escaped + user.Set("name", ""+user.GetString("name")+"
") + err := mails.SendRecordOTP(testApp, user, "test_otp_id", "test_otp_code") if err != nil { t.Fatal(err) @@ -140,7 +156,7 @@ func TestSendRecordOTP(t *testing.T) { } expectedParts := []string{ - user.GetString("name") + "{RECORD:tokenKey}", // the record name as {RECORD:name} + html.EscapeString(user.GetString("name")) + "{RECORD:tokenKey}", // the record name as {RECORD:name} "one-time password", "test_otp_code", }