From 1c48e513849e3849e2c8152ac1dbbab7b342d11b Mon Sep 17 00:00:00 2001 From: Nicola Murino Date: Thu, 24 Apr 2025 19:01:17 +0200 Subject: [PATCH] EventManager: escape email body when content type is text/html Signed-off-by: Nicola Murino --- internal/common/eventmanager.go | 80 ++++++++++++++++------------ internal/common/eventmanager_test.go | 6 +-- internal/common/protocol_test.go | 7 ++- 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/internal/common/eventmanager.go b/internal/common/eventmanager.go index 17e0cd03..02fae490 100644 --- a/internal/common/eventmanager.go +++ b/internal/common/eventmanager.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "fmt" + "html" "io" "mime" "mime/multipart" @@ -775,14 +776,18 @@ func (p *EventParams) getRetentionReportsAsMailAttachment() (*mail.File, error) }, nil } -func (*EventParams) getStringReplacement(val string, jsonEscaped bool) string { - if jsonEscaped { +func (*EventParams) getStringReplacement(val string, escapeMode int) string { + switch escapeMode { + case 1: return util.JSONEscape(val) + case 2: + return html.EscapeString(val) + default: + return val } - return val } -func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []string { +func (p *EventParams) getStringReplacements(addObjectData bool, escapeMode int) []string { var dateTimeString string if Config.TZ == "local" { dateTimeString = p.Timestamp.Local().Format(dateTimeMillisFormat) @@ -796,23 +801,23 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s minute := dateTimeString[14:16] replacements := []string{ - "{{.Name}}", p.getStringReplacement(p.Name, jsonEscaped), + "{{.Name}}", p.getStringReplacement(p.Name, escapeMode), "{{.Event}}", p.Event, "{{.Status}}", fmt.Sprintf("%d", p.Status), - "{{.VirtualPath}}", p.getStringReplacement(p.VirtualPath, jsonEscaped), - "{{.EscapedVirtualPath}}", p.getStringReplacement(url.QueryEscape(p.VirtualPath), jsonEscaped), - "{{.FsPath}}", p.getStringReplacement(p.FsPath, jsonEscaped), - "{{.VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, jsonEscaped), - "{{.FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, jsonEscaped), - "{{.ObjectName}}", p.getStringReplacement(p.ObjectName, jsonEscaped), - "{{.ObjectBaseName}}", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), jsonEscaped), + "{{.VirtualPath}}", p.getStringReplacement(p.VirtualPath, escapeMode), + "{{.EscapedVirtualPath}}", p.getStringReplacement(url.QueryEscape(p.VirtualPath), escapeMode), + "{{.FsPath}}", p.getStringReplacement(p.FsPath, escapeMode), + "{{.VirtualTargetPath}}", p.getStringReplacement(p.VirtualTargetPath, escapeMode), + "{{.FsTargetPath}}", p.getStringReplacement(p.FsTargetPath, escapeMode), + "{{.ObjectName}}", p.getStringReplacement(p.ObjectName, escapeMode), + "{{.ObjectBaseName}}", p.getStringReplacement(strings.TrimSuffix(p.ObjectName, p.Extension), escapeMode), "{{.ObjectType}}", p.ObjectType, "{{.FileSize}}", strconv.FormatInt(p.FileSize, 10), "{{.Elapsed}}", strconv.FormatInt(p.Elapsed, 10), "{{.Protocol}}", p.Protocol, "{{.IP}}", p.IP, - "{{.Role}}", p.getStringReplacement(p.Role, jsonEscaped), - "{{.Email}}", p.getStringReplacement(p.Email, jsonEscaped), + "{{.Role}}", p.getStringReplacement(p.Role, escapeMode), + "{{.Email}}", p.getStringReplacement(p.Email, escapeMode), "{{.Timestamp}}", strconv.FormatInt(p.Timestamp.UnixNano(), 10), "{{.DateTime}}", dateTimeString, "{{.Year}}", year, @@ -821,18 +826,18 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s "{{.Hour}}", hour, "{{.Minute}}", minute, "{{.StatusString}}", p.getStatusString(), - "{{.UID}}", p.getStringReplacement(p.UID, jsonEscaped), - "{{.Ext}}", p.getStringReplacement(p.Extension, jsonEscaped), + "{{.UID}}", p.getStringReplacement(p.UID, escapeMode), + "{{.Ext}}", p.getStringReplacement(p.Extension, escapeMode), } if p.VirtualPath != "" { - replacements = append(replacements, "{{.VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), jsonEscaped)) + replacements = append(replacements, "{{.VirtualDirPath}}", p.getStringReplacement(path.Dir(p.VirtualPath), escapeMode)) } if p.VirtualTargetPath != "" { - replacements = append(replacements, "{{.VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), jsonEscaped)) - replacements = append(replacements, "{{.TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), jsonEscaped)) + replacements = append(replacements, "{{.VirtualTargetDirPath}}", p.getStringReplacement(path.Dir(p.VirtualTargetPath), escapeMode)) + replacements = append(replacements, "{{.TargetName}}", p.getStringReplacement(path.Base(p.VirtualTargetPath), escapeMode)) } if len(p.errors) > 0 { - replacements = append(replacements, "{{.ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), jsonEscaped)) + replacements = append(replacements, "{{.ErrorString}}", p.getStringReplacement(strings.Join(p.errors, ", "), escapeMode)) } else { replacements = append(replacements, "{{.ErrorString}}", "") } @@ -842,13 +847,13 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s data, err := p.Object.RenderAsJSON(p.Event != operationDelete) if err == nil { dataString := util.BytesToString(data) - replacements[len(replacements)-3] = p.getStringReplacement(dataString, false) - replacements[len(replacements)-1] = p.getStringReplacement(dataString, true) + replacements[len(replacements)-3] = p.getStringReplacement(dataString, 0) + replacements[len(replacements)-1] = p.getStringReplacement(dataString, 1) } } if p.IDPCustomFields != nil { for k, v := range *p.IDPCustomFields { - replacements = append(replacements, fmt.Sprintf("{{.IDPField%s}}", k), p.getStringReplacement(v, jsonEscaped)) + replacements = append(replacements, fmt.Sprintf("{{.IDPField%s}}", k), p.getStringReplacement(v, escapeMode)) } } replacements = append(replacements, "{{.Metadata}}", "{}") @@ -857,8 +862,8 @@ func (p *EventParams) getStringReplacements(addObjectData, jsonEscaped bool) []s data, err := json.Marshal(p.Metadata) if err == nil { dataString := util.BytesToString(data) - replacements[len(replacements)-3] = p.getStringReplacement(dataString, false) - replacements[len(replacements)-1] = p.getStringReplacement(dataString, true) + replacements[len(replacements)-3] = p.getStringReplacement(dataString, 0) + replacements[len(replacements)-1] = p.getStringReplacement(dataString, 1) } } return replacements @@ -1314,7 +1319,7 @@ func writeHTTPPart(m *multipart.Writer, part dataprovider.HTTPPart, h textproto. if part.Body != "" { cType := h.Get("Content-Type") if strings.Contains(strings.ToLower(cType), "application/json") { - replacements := params.getStringReplacements(addObjectData, true) + replacements := params.getStringReplacements(addObjectData, 1) jsonReplacer := strings.NewReplacer(replacements...) _, err = partWriter.Write(util.StringToBytes(replaceWithReplacer(part.Body, jsonReplacer))) } else { @@ -1362,7 +1367,7 @@ func getHTTPRuleActionBody(c *dataprovider.EventActionHTTPConfig, replacer *stri return bytes.NewBuffer(data), "", nil } if c.HasJSONBody() { - replacements := params.getStringReplacements(addObjectData, true) + replacements := params.getStringReplacements(addObjectData, 1) jsonReplacer := strings.NewReplacer(replacements...) return bytes.NewBufferString(replaceWithReplacer(c.Body, jsonReplacer)), "", nil } @@ -1450,7 +1455,7 @@ func executeHTTPRuleAction(c dataprovider.EventActionHTTPConfig, params *EventPa addObjectData = c.HasObjectData() } - replacements := params.getStringReplacements(addObjectData, false) + replacements := params.getStringReplacements(addObjectData, 0) replacer := strings.NewReplacer(replacements...) endpoint, err := getHTTPRuleActionEndpoint(&c, replacer) if err != nil { @@ -1521,7 +1526,7 @@ func executeCommandRuleAction(c dataprovider.EventActionCommandConfig, params *E } } } - replacements := params.getStringReplacements(addObjectData, false) + replacements := params.getStringReplacements(addObjectData, 0) replacer := strings.NewReplacer(replacements...) args := make([]string, 0, len(c.Args)) @@ -1576,9 +1581,16 @@ func executeEmailRuleAction(c dataprovider.EventActionEmailConfig, params *Event addObjectData = true } } - replacements := params.getStringReplacements(addObjectData, false) + replacements := params.getStringReplacements(addObjectData, 0) replacer := strings.NewReplacer(replacements...) - body := replaceWithReplacer(c.Body, replacer) + var body string + if c.ContentType == 1 { + replacements := params.getStringReplacements(addObjectData, 2) + bodyReplacer := strings.NewReplacer(replacements...) + body = replaceWithReplacer(c.Body, bodyReplacer) + } else { + body = replaceWithReplacer(c.Body, replacer) + } subject := replaceWithReplacer(c.Subject, replacer) recipients := getEmailAddressesWithReplacer(c.Recipients, replacer) bcc := getEmailAddressesWithReplacer(c.Bcc, replacer) @@ -2150,7 +2162,7 @@ func executeFsRuleAction(c dataprovider.EventActionFilesystemConfig, conditions params *EventParams, ) error { addObjectData := false - replacements := params.getStringReplacements(addObjectData, false) + replacements := params.getStringReplacements(addObjectData, 0) replacer := strings.NewReplacer(replacements...) switch c.Type { case dataprovider.FilesystemActionRename: @@ -2550,7 +2562,7 @@ func executeAdminCheckAction(c *dataprovider.EventActionIDPAccountCheck, params return nil, err } - replacements := params.getStringReplacements(false, true) + replacements := params.getStringReplacements(false, 1) replacer := strings.NewReplacer(replacements...) data := replaceWithReplacer(c.TemplateAdmin, replacer) @@ -2620,7 +2632,7 @@ func executeUserCheckAction(c *dataprovider.EventActionIDPAccountCheck, params * if err != nil && !errors.Is(err, util.ErrNotFound) { return nil, err } - replacements := params.getStringReplacements(false, true) + replacements := params.getStringReplacements(false, 1) replacer := strings.NewReplacer(replacements...) data := replaceWithReplacer(c.TemplateUser, replacer) diff --git a/internal/common/eventmanager_test.go b/internal/common/eventmanager_test.go index d3d915d6..d350421e 100644 --- a/internal/common/eventmanager_test.go +++ b/internal/common/eventmanager_test.go @@ -808,7 +808,7 @@ func TestDateTimePlaceholder(t *testing.T) { params := EventParams{ Timestamp: dateTime, } - replacements := params.getStringReplacements(false, false) + replacements := params.getStringReplacements(false, 0) r := strings.NewReplacer(replacements...) res := r.Replace("{{.DateTime}}") assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat), res) @@ -816,7 +816,7 @@ func TestDateTimePlaceholder(t *testing.T) { assert.Equal(t, dateTime.UTC().Format(dateTimeMillisFormat)[:16], res) Config.TZ = "local" - replacements = params.getStringReplacements(false, false) + replacements = params.getStringReplacements(false, 0) r = strings.NewReplacer(replacements...) res = r.Replace("{{.DateTime}}") assert.Equal(t, dateTime.Local().Format(dateTimeMillisFormat), res) @@ -2331,7 +2331,7 @@ func TestMetadataReplacement(t *testing.T) { "key": "value", }, } - replacements := params.getStringReplacements(false, false) + replacements := params.getStringReplacements(false, 0) replacer := strings.NewReplacer(replacements...) reader, _, err := getHTTPRuleActionBody(&dataprovider.EventActionHTTPConfig{Body: "{{.Metadata}} {{.MetadataString}}"}, replacer, nil, dataprovider.User{}, params, false) require.NoError(t, err) diff --git a/internal/common/protocol_test.go b/internal/common/protocol_test.go index 9f57e272..0eabd871 100644 --- a/internal/common/protocol_test.go +++ b/internal/common/protocol_test.go @@ -6966,7 +6966,7 @@ func TestEventRuleRenameEvent(t *testing.T) { Recipients: []string{"test@example.com"}, Subject: `"{{.Event}}" from "{{.Name}}"`, ContentType: 1, - Body: `

Fs path {{.FsPath}}, Target path "{{.VirtualTargetDirPath}}/{{.TargetName}}", size: {{.FileSize}}

`, + Body: `

Fs path {{.FsPath}}, Name: {{.Name}}, Target path "{{.VirtualTargetDirPath}}/{{.TargetName}}", size: {{.FileSize}}

`, }, }, } @@ -6991,7 +6991,9 @@ func TestEventRuleRenameEvent(t *testing.T) { rule1, _, err := httpdtest.AddEventRule(r1, http.StatusCreated) assert.NoError(t, err) - user, _, err := httpdtest.AddUser(getTestUser(), http.StatusCreated) + u := getTestUser() + u.Username = "test chars" + user, _, err := httpdtest.AddUser(u, http.StatusCreated) assert.NoError(t, err) conn, client, err := getSftpClient(user) if assert.NoError(t, err) { @@ -7015,6 +7017,7 @@ func TestEventRuleRenameEvent(t *testing.T) { assert.Contains(t, email.Data, fmt.Sprintf(`Subject: "rename" from "%s"`, user.Username)) assert.Contains(t, email.Data, "Content-Type: text/html") assert.Contains(t, email.Data, fmt.Sprintf("Target path %q", path.Join("/subdir", testFileName))) + assert.Contains(t, email.Data, "Name: test <html > chars,") } _, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)