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
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
87 changes: 82 additions & 5 deletions services/tasks/alert.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ var templates embed.FS

// Alert represents an alert that will be templated and sent to the appropriate service
type Alert struct {
Name string
Author string
Color string
Task alertTask
Chat alertChat
Name string
Author string
Color string
Task alertTask
Chat alertChat
Pushover alertPushover
}

type alertTask struct {
Expand All @@ -39,6 +40,11 @@ type alertChat struct {
ID string
}

type alertPushover struct {
Token string // application/API token
User string // global user OR target group key
}

func (t *TaskRunner) sendMailAlert() {
if !util.Config.EmailAlert || !t.alert {
return
Expand Down Expand Up @@ -190,6 +196,77 @@ func (t *TaskRunner) sendTelegramAlert() {
}
}

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

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

token := util.Config.PushoverAPIToken
user := util.Config.PushoverUserGroupKey

if token == "" || user == "" {
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,
},
Pushover: alertPushover{Token: token, User: user},
}

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(
"https://api.pushover.net/1/messages.json",
"application/json",
body,
)

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) sendSlackAlert() {
if !util.Config.SlackAlert || !t.alert {
return
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
9 changes: 9 additions & 0 deletions services/tasks/templates/pushover.tmpl
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"token": "{{ .Pushover.Token }}",
"user": "{{ .Pushover.User }}",
"title": "{{ .Name }} #{{ .Task.ID }}: {{ .Task.Result }}",
"html": 1,
"message": "<b>{{ .Task.Result }}</b> {{ .Task.Version }} - {{ .Task.Desc }}\nby {{ .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.

Severity: Medium — JSON injection / alert manipulation

This template builds JSON with text/template and no escaping. {{ .Task.Desc }} is populated from Task.Message, which task starters can set via POST /api/project/{id}/tasks (CanRunProjectTasks). Template name and author are also attacker-influenced.

Attack path: start a task with a crafted message such as x"}, "user": "<attacker_pushover_key>", "priority": 2, "x": ". When the alert fires, injected keys can override Pushover API fields (recipient user, priority, etc.) because duplicate JSON keys are commonly last-wins.

Impact: redirect operational alerts to an attacker-controlled Pushover account (information disclosure) or manipulate notification metadata; phishing via injected url is also possible depending on parser behavior.

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 — JSON injection / alert hijacking

This template builds JSON with text/template and no escaping. {{ .Task.Desc }} is populated from Task.Message, which task starters supply via POST /api/project/{id}/tasks (requires CanRunProjectTasks). {{ .Author }} and template {{ .Name }} are also attacker-influenced.

Attack path: start a task with a crafted message such as x"}, "user": "<attacker_pushover_key>", "priority": 2, "x": ". When the alert fires, injected keys appear after the legitimate user field; duplicate-key parsers (including typical JSON decoders) are last-wins, so the notification is redirected to the attacker's Pushover account using the server's application token.

Impact: operational alert hijacking (information disclosure) and possible phishing via injected url/title fields.

Fix: build the request with encoding/json (or at minimum json template func escaping) instead of hand-assembled JSON strings.

"url": "{{ .Task.URL }}",
"url_title": "View task in Semaphore"
}
4 changes: 4 additions & 0 deletions util/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,10 @@ type ConfigType struct {
GotifyUrl string `json:"gotify_url,omitempty" env:"SEMAPHORE_GOTIFY_URL"`
GotifyToken string `json:"gotify_token,omitempty" env:"SEMAPHORE_GOTIFY_TOKEN,sensitive"`

PushoverAlert bool `json:"pushover_alert,omitempty" env:"SEMAPHORE_PUSHOVER_ALERT"`
PushoverUserGroupKey string `json:"pushover_user_group_key,omitempty" env:"SEMAPHORE_PUSHOVER_USER_GROUP_KEY"`
PushoverAPIToken string `json:"pushover_api_token,omitempty" env:"SEMAPHORE_PUSHOVER_API_TOKEN,sensitive"`

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

Expand Down
Loading
Loading