Email validation

This commit is contained in:
binwiederhier
2026-03-16 21:03:33 -04:00
parent 2ad78edca1
commit d9efe50848
4 changed files with 34 additions and 1 deletions

View File

@@ -1798,4 +1798,8 @@ and the [ntfy Android app](https://github.com/binwiederhier/ntfy-android/release
## Not released yet ## Not released yet
Nothing. ### ntfy server v2.20.x (UNRELEASED)
**Bug fixes + maintenance:**
* Reject invalid e-mail addresses (e.g. multiple comma-separated recipients) with HTTP 400

View File

@@ -142,6 +142,7 @@ var (
errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileNotFound = &errHTTP{40047, http.StatusBadRequest, "invalid request: template file not found", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil} errHTTPBadRequestTemplateFileInvalid = &errHTTP{40048, http.StatusBadRequest, "invalid request: template file invalid", "https://ntfy.sh/docs/publish/#message-templating", nil}
errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil} errHTTPBadRequestSequenceIDInvalid = &errHTTP{40049, http.StatusBadRequest, "invalid request: sequence ID invalid", "https://ntfy.sh/docs/publish/#updating-deleting-notifications", nil}
errHTTPBadRequestEmailAddressInvalid = &errHTTP{40050, http.StatusBadRequest, "invalid request: invalid e-mail address", "https://ntfy.sh/docs/publish/#e-mail-notifications", nil}
errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", "", nil}
errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication", nil}
errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication", nil}

View File

@@ -122,6 +122,7 @@ var (
fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`) fileRegex = regexp.MustCompile(`^/file/([-_A-Za-z0-9]{1,64})(?:\.[A-Za-z0-9]{1,16})?$`)
urlRegex = regexp.MustCompile(`^https?://`) urlRegex = regexp.MustCompile(`^https?://`)
phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`) phoneNumberRegex = regexp.MustCompile(`^\+\d{1,100}$`)
emailAddressRegex = regexp.MustCompile(`^[^\s,;]+@[^\s,;]+$`)
//go:embed site //go:embed site
webFs embed.FS webFs embed.FS
@@ -1163,6 +1164,9 @@ func (s *Server) parsePublishParams(r *http.Request, m *model.Message) (cache bo
m.Icon = icon m.Icon = icon
} }
email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e") email = readParam(r, "x-email", "x-e-mail", "email", "e-mail", "mail", "e")
if email != "" && !emailAddressRegex.MatchString(email) {
return false, false, "", "", "", false, "", errHTTPBadRequestEmailAddressInvalid
}
if s.smtpSender == nil && email != "" { if s.smtpSender == nil && email != "" {
return false, false, "", "", "", false, "", errHTTPBadRequestEmailDisabled return false, false, "", "", "", false, "", errHTTPBadRequestEmailDisabled
} }

View File

@@ -1543,6 +1543,30 @@ func TestServer_PublishEmailNoMailer_Fail(t *testing.T) {
}) })
} }
func TestServer_PublishEmailAddressInvalid(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) {
s := newTestServer(t, newTestConfig(t, databaseURL))
s.smtpSender = &testMailer{}
addresses := []string{
"test@example.com, other@example.com",
"invalidaddress",
"@nope",
"nope@",
}
for _, email := range addresses {
response := request(t, s, "PUT", "/mytopic", "fail", map[string]string{
"E-Mail": email,
})
require.Equal(t, 400, response.Code, "expected 400 for email: %s", email)
}
// Valid address should succeed
response := request(t, s, "PUT", "/mytopic", "success", map[string]string{
"E-Mail": "test@example.com",
})
require.Equal(t, 200, response.Code)
})
}
func TestServer_PublishAndExpungeTopicAfter16Hours(t *testing.T) { func TestServer_PublishAndExpungeTopicAfter16Hours(t *testing.T) {
forEachBackend(t, func(t *testing.T, databaseURL string) { forEachBackend(t, func(t *testing.T, databaseURL string) {
t.Parallel() t.Parallel()