EventManager: escape email body when content type is text/html

Signed-off-by: Nicola Murino <nicola.murino@gmail.com>
This commit is contained in:
Nicola Murino
2025-04-24 19:01:17 +02:00
parent 5efd232809
commit 1c48e51384
3 changed files with 54 additions and 39 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -6966,7 +6966,7 @@ func TestEventRuleRenameEvent(t *testing.T) {
Recipients: []string{"test@example.com"},
Subject: `"{{.Event}}" from "{{.Name}}"`,
ContentType: 1,
Body: `<p>Fs path {{.FsPath}}, Target path "{{.VirtualTargetDirPath}}/{{.TargetName}}", size: {{.FileSize}}</p>`,
Body: `<p>Fs path {{.FsPath}}, Name: {{.Name}}, Target path "{{.VirtualTargetDirPath}}/{{.TargetName}}", size: {{.FileSize}}</p>`,
},
},
}
@@ -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 <html > 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 &lt;html &gt; chars,")
}
_, err = httpdtest.RemoveEventRule(rule1, http.StatusOK)