From b805d49cfd4ca66c37c1a857035a27abcf1e3ccf Mon Sep 17 00:00:00 2001 From: Philipp Heckel Date: Sat, 23 Apr 2022 13:40:26 -0400 Subject: [PATCH] Disallow HEAD/GET requests with body --- server/errors.go | 12 +++++++++++- server/server.go | 2 +- server/util.go | 21 ++++++++++++--------- 3 files changed, 24 insertions(+), 11 deletions(-) diff --git a/server/errors.go b/server/errors.go index 98cce9f4..32c1b3b9 100644 --- a/server/errors.go +++ b/server/errors.go @@ -2,6 +2,7 @@ package server import ( "encoding/json" + "fmt" "net/http" ) @@ -22,6 +23,15 @@ func (e errHTTP) JSON() string { return string(b) } +func wrapErrHTTP(err *errHTTP, message string, args ...interface{}) *errHTTP { + return &errHTTP{ + Code: err.Code, + HTTPCode: err.HTTPCode, + Message: fmt.Sprintf("%s, %s", err.Message, fmt.Sprintf(message, args...)), + Link: err.Link, + } +} + var ( errHTTPBadRequestEmailDisabled = &errHTTP{40001, http.StatusBadRequest, "e-mail notifications are not enabled", "https://ntfy.sh/docs/config/#e-mail-notifications"} errHTTPBadRequestDelayNoCache = &errHTTP{40002, http.StatusBadRequest, "cannot disable cache for delayed message", ""} @@ -39,7 +49,7 @@ var ( errHTTPBadRequestAttachmentsExpiryBeforeDelivery = &errHTTP{40015, http.StatusBadRequest, "invalid request: attachment expiry before delayed delivery date", "https://ntfy.sh/docs/publish/#scheduled-delivery"} errHTTPBadRequestWebSocketsUpgradeHeaderMissing = &errHTTP{40016, http.StatusBadRequest, "invalid request: client not using the websocket protocol", "https://ntfy.sh/docs/subscribe/api/#websockets"} errHTTPBadRequestJSONInvalid = &errHTTP{40017, http.StatusBadRequest, "invalid request: request body must be message JSON", "https://ntfy.sh/docs/publish/#publish-as-json"} - errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions not valid", "https://ntfy.sh/docs/publish/#action-buttons"} + errHTTPBadRequestActionsInvalid = &errHTTP{40018, http.StatusBadRequest, "invalid request: actions invalid", "https://ntfy.sh/docs/publish/#action-buttons"} errHTTPNotFound = &errHTTP{40401, http.StatusNotFound, "page not found", ""} errHTTPUnauthorized = &errHTTP{40101, http.StatusUnauthorized, "unauthorized", "https://ntfy.sh/docs/publish/#authentication"} errHTTPForbidden = &errHTTP{40301, http.StatusForbidden, "forbidden", "https://ntfy.sh/docs/publish/#authentication"} diff --git a/server/server.go b/server/server.go index ab897da8..b3336da3 100644 --- a/server/server.go +++ b/server/server.go @@ -539,7 +539,7 @@ func (s *Server) parsePublishParams(r *http.Request, v *visitor, m *message) (ca if actionsStr != "" { m.Actions, err = parseActions(actionsStr) if err != nil { - return false, false, "", false, errHTTPBadRequestActionsInvalid + return false, false, "", false, err // wrapped errHTTPBadRequestActionsInvalid } } unifiedpush = readBoolParam(r, false, "x-unifiedpush", "unifiedpush", "up") // see GET too! diff --git a/server/util.go b/server/util.go index 22e5fac6..c51b6798 100644 --- a/server/util.go +++ b/server/util.go @@ -2,7 +2,6 @@ package server import ( "encoding/json" - "fmt" "heckel.io/ntfy/util" "net/http" "strings" @@ -61,22 +60,26 @@ func parseActions(s string) (actions []*action, err error) { return nil, err } - // Add ID field + // Add ID field, ensure correct uppercase/lowercase for i := range actions { actions[i].ID = util.RandomString(actionIDLength) + actions[i].Action = strings.ToLower(actions[i].Action) + actions[i].Method = strings.ToUpper(actions[i].Method) } // Validate if len(actions) > actionsMax { - return nil, fmt.Errorf("too many actions, only %d allowed", actionsMax) + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "only %d actions allowed", actionsMax) } for _, action := range actions { if !util.InStringList([]string{"view", "broadcast", "http"}, action.Action) { - return nil, fmt.Errorf("cannot parse actions: action '%s' unknown", action.Action) + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "action '%s' unknown", action.Action) } else if action.Label == "" { - return nil, fmt.Errorf("cannot parse actions: label must be set") + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'label' is required") } else if util.InStringList([]string{"view", "http"}, action.Action) && action.URL == "" { - return nil, fmt.Errorf("parameter 'url' is required for action '%s'", action.Action) + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'url' is required for action '%s'", action.Action) + } else if action.Action == "http" && util.InStringList([]string{"GET", "HEAD"}, action.Method) && action.Body != "" { + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "parameter 'body' cannot be set if method is %s", action.Method) } } @@ -101,7 +104,7 @@ func parseActionsFromSimple(s string) ([]*action, error) { } parts := util.SplitNoEmpty(rawAction, ",") if len(parts) < 3 { - return nil, fmt.Errorf("cannot parse action: action requires at least keys 'action', 'label' and one parameter: %s", rawAction) + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "action requires at least keys 'action', 'label' and one parameter: %s", rawAction) } for i, part := range parts { key, value := util.SplitKV(part, "=") @@ -131,10 +134,10 @@ func parseActionsFromSimple(s string) ([]*action, error) { case "body": newAction.Body = value default: - return nil, fmt.Errorf("cannot parse action: key '%s' not supported, please use JSON format instead", part) + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "key '%s' unknown", key) } } else { - return nil, fmt.Errorf("cannot parse action: unknown phrase '%s'", part) + return nil, wrapErrHTTP(errHTTPBadRequestActionsInvalid, "unknown term '%s'", part) } } actions = append(actions, newAction)