Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions cli/setup/setup.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,12 @@ func InteractiveSetup(conf *util.ConfigType) {
askValue("Microsoft Teams Webhook URL", "", &conf.MicrosoftTeamsUrl)
}

askConfirmation("Enable Pushover alerts?", false, &conf.PushoverAlert)
if conf.PushoverAlert {
askValue("Pushover User KEY", "", &conf.PushoverUserKey)
askValue("Pushover Token", "", &conf.PushoverToken)
}

askConfirmation("Enable LDAP authentication?", false, &conf.LdapEnable)
if conf.LdapEnable {
conf.LdapMappings = &util.LdapMappings{}
Expand Down
1 change: 1 addition & 0 deletions services/tasks/TaskRunner_logging.go
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ func (t *TaskRunner) SetStatus(status task_logger.TaskStatus) {
t.sendMicrosoftTeamsAlert()
t.sendDingTalkAlert()
t.sendGotifyAlert()
t.sendPushoverAlert()
}

for _, l := range t.statusListeners {
Expand Down
70 changes: 69 additions & 1 deletion services/tasks/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ type alertTask struct {
Result string
Desc string
Version string
Status string
}

type alertChat struct {
Expand Down Expand Up @@ -504,6 +505,73 @@ func (t *TaskRunner) sendGotifyAlert() {
}
}

func (t *TaskRunner) sendPushoverAlert() {
if !util.Config.PushoverAlert || !t.alert {
return
}

if t.Template.SuppressSuccessAlerts && t.Task.Status == task_logger.TaskSuccessStatus {
return
}

body := bytes.NewBufferString("")
author, version := t.alertInfos()

alert := Alert{
Name: t.Template.Name,
Author: author,
Color: t.alertColor("pushover"),
Task: alertTask{
ID: strconv.Itoa(t.Task.ID),
URL: t.taskLink(),
Result: t.Task.Status.Format(),
Version: version,
Desc: t.Task.Message,
Status: string(t.Task.Status),
},
}

tpl, err := template.ParseFS(templates, "templates/pushover.tmpl")

if err != nil {
t.Log("Can't parse pushover alert template!")
panic(err)
}

if err := tpl.Execute(body, alert); err != nil {
t.Log("Can't generate pushover alert template!")
panic(err)
}

if body.Len() == 0 {
t.Log("Buffer for pushover alert is empty")
return
}

t.Log("Attempting to send pushover alert")

resp, err := http.Post(
fmt.Sprintf(
"https://api.pushover.net/1/messages.json?user=%s&token=%s",
util.Config.PushoverUserKey,
util.Config.PushoverToken),
"application/json",
body,
Comment on lines +551 to +559
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: Medium — Secret leakage via error logs

The Pushover user key and token are embedded in the URL query string. When http.Post fails (DNS error, TLS failure, timeout, connection refused, etc.), Go's net/http includes the full URL in the returned error message. That error is then passed to t.Log() on line 563, which broadcasts it to all project users via WebSocket and persists it in the task log database.

This leaks server-level configuration secrets to any user with access to the project.

// Current — credentials in URL, leaked on error:
t.Log("Can't send pushover alert! Error: " + err.Error())
// err.Error() → `Post "https://api.pushover.net/1/messages.json?user=SECRET&token=SECRET": dial tcp: ...`

The Pushover API accepts user and token as fields in the JSON POST body, so there is no need to put them in the URL. Recommended fix: move user and token into the JSON template body and POST to the bare endpoint https://api.pushover.net/1/messages.json.

// Fixed — credentials in body, not URL:
resp, err := http.Post(
    "https://api.pushover.net/1/messages.json",
    "application/json",
    body,
)

And add to pushover.tmpl:

{
  "user": "{{.PushoverUserKey}}",
  "token": "{{.PushoverToken}}",
  ...
}

(Note: the same pattern exists in the Gotify and Telegram alert senders, but those APIs require the token in the URL path/query. Pushover does not.)

)

if err != nil {
t.Log("Can't send pushover alert! Error: " + err.Error())
} else if resp.StatusCode != 200 {
t.Log("Can't send pushover alert! Response code: " + strconv.Itoa(resp.StatusCode))
} else {
t.Log("Sent successfully pushover alert")
}

if resp != nil {
defer resp.Body.Close() //nolint:errcheck
}
}

func (t *TaskRunner) alertInfos() (string, string) {
version := ""

Expand Down Expand Up @@ -552,7 +620,7 @@ func (t *TaskRunner) alertColor(kind string) string {
case task_logger.TaskStoppedStatus:
return "#5B5B5B"
}
case "rocketchat":
case "rocketchat", "pushover":
switch t.Task.Status {
case task_logger.TaskSuccessStatus:
return "#00EE00"
Expand Down
1 change: 1 addition & 0 deletions services/tasks/alert_test_sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ func SendProjectTestAlerts(project db.Project, store db.Store) (err error) {
tr.sendMicrosoftTeamsAlert()
tr.sendDingTalkAlert()
tr.sendGotifyAlert()
tr.sendPushoverAlert()
tr.sendMailAlert()

return
Expand Down
8 changes: 8 additions & 0 deletions services/tasks/templates/pushover.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
{
"html": 1,
"priority": {{ if eq .Task.Status "error" }}1{{ else }}0{{ end }},
"title": "Task: {{ .Name }}",
"message": "Execution #: {{ .Task.ID }}\n<font color=\"{{ .Color }}\"><b>{{ .Task.Result }}</b></font>\n{{ if .Task.Version }}Version: {{ .Task.Version }}\n{{ end }}\nDescription: {{ .Task.Desc }}\nAuthor: {{ .Author }}",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Observation (Low, codebase-wide pattern) — JSON injection via text/template

User-controlled values (.Name from template name, .Task.Desc from task message, .Author from username) are interpolated into JSON using Go's text/template, which performs no escaping. A " character in any of these fields breaks the JSON structure.

For example, a task message of pwned", "priority": 2, "retry": 30, "expire": 86400, "x": " would inject an emergency-priority notification that repeats every 30 seconds.

This is the same pattern used by all other alert templates in this codebase (telegram, slack, rocketchat, etc.), so it is not a regression unique to this PR. Flagging for awareness — the proper fix would be to use encoding/json marshaling or a custom template function for JSON-safe escaping across all alert senders.

"url": "{{ .Task.URL }}",
"url_title": "Task URL"
}
3 changes: 3 additions & 0 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -335,6 +335,9 @@ type ConfigType struct {
GotifyAlert bool `json:"gotify_alert,omitempty" env:"SEMAPHORE_GOTIFY_ALERT"`
GotifyUrl string `json:"gotify_url,omitempty" env:"SEMAPHORE_GOTIFY_URL"`
GotifyToken string `json:"gotify_token,omitempty" env:"SEMAPHORE_GOTIFY_TOKEN"`
PushoverAlert bool `json:"pushover_alert,omitempty" env:"SEMAPHORE_PUSHOVER_ALERT"`
PushoverUserKey string `json:"pushover_user_key,omitempty" env:"SEMAPHORE_PUSHOVER_USER_KEY"`
PushoverToken string `json:"pushover_token,omitempty" env:"SEMAPHORE_PUSHOVER_TOKEN"`

// oidc settings
OidcProviders map[string]OidcProvider `json:"oidc_providers,omitempty" env:"SEMAPHORE_OIDC_PROVIDERS"`
Expand Down
Loading