Skip to content
Open
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
50 changes: 37 additions & 13 deletions pkg/providers/teams/teams.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package teams

import (
"bytes"
"fmt"
"net/http"
"strings"

"github.com/containrrr/shoutrrr"
Expand Down Expand Up @@ -43,19 +45,41 @@ func (p *Provider) Send(message, CliFormat string) error {
p.counter++
for _, pr := range p.Teams {
msg := utils.FormatMessage(message, utils.SelectFormat(CliFormat, pr.TeamsFormat), p.counter)
webhookParts := strings.Split(pr.TeamsWebHookURL, "/webhookb2/")
if len(webhookParts) != 2 {
err := fmt.Errorf("teams: invalid webhook url for id: %s ", pr.ID)
TeamsErr = multierr.Append(TeamsErr, err)
}
teamsHost := strings.TrimPrefix(webhookParts[0], "https://")
teamsTokens := strings.ReplaceAll(webhookParts[1], "IncomingWebhook/", "")
url := fmt.Sprintf("teams://%s?host=%s", teamsTokens, teamsHost)
err := shoutrrr.Send(url, msg)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to send teams notification for id: %s ", pr.ID))
TeamsErr = multierr.Append(TeamsErr, err)
continue
provider := strings.Split(pr.TeamsWebHookURL, "/")[3]

// Deprecated method
if provider == "webhookb2" {
webhookParts := strings.Split(pr.TeamsWebHookURL, "/webhookb2/")
if len(webhookParts) != 2 {
err := fmt.Errorf("teams: invalid webhook url for id: %s ", pr.ID)
TeamsErr = multierr.Append(TeamsErr, err)
}
teamsHost := strings.TrimPrefix(webhookParts[0], "https://")
teamsTokens := strings.ReplaceAll(webhookParts[1], "IncomingWebhook/", "")
url := fmt.Sprintf("teams://%s?host=%s", teamsTokens, teamsHost)
err := shoutrrr.Send(url, msg)
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to send webhook teams notification for id: %s", pr.ID))
TeamsErr = multierr.Append(TeamsErr, err)
continue
}

// New Power Automate method
} else if provider == "workflows" {
htmlMessage := strings.ReplaceAll(msg, "\n", "<br>")
payload := fmt.Sprintf(`{"text": "%s"}`, htmlMessage)
Comment on lines +67 to +70
Copy link
Copy Markdown

@coderabbitai coderabbitai bot Mar 28, 2025

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

Improve JSON payload creation for better security.

The current implementation has two potential issues:

  1. Simple replacement of newlines with HTML breaks might not handle other HTML special characters
  2. Using string formatting to construct JSON can lead to malformed JSON if the message contains quotes or special characters
- htmlMessage := strings.ReplaceAll(msg, "\n", "<br>")
- payload := fmt.Sprintf(`{"text": "%s"}`, htmlMessage)
+ import "encoding/json"
+ import "html"
+
+ // HTML encode message to escape special characters
+ htmlMessage := html.EscapeString(strings.ReplaceAll(msg, "\n", "<br>"))
+ 
+ // Create payload using proper JSON marshaling
+ type teamsPayload struct {
+     Text string `json:"text"`
+ }
+ payloadObj := teamsPayload{Text: htmlMessage}
+ payloadBytes, err := json.Marshal(payloadObj)
+ if err != nil {
+     err = errors.Wrap(err, fmt.Sprintf("failed to create JSON payload for teams notification id: %s", pr.ID))
+     TeamsErr = multierr.Append(TeamsErr, err)
+     continue
+ }

Committable suggestion skipped: line range outside the PR's diff.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

+1

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@dogancanbakir Thanks for confirming! I'm glad you found the suggestion helpful. Let me know if you need any further assistance with the implementation!

(_/)
(^.^)
/>🎯 Bullseye!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@coderabbitai make the necessary changes in this pr

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@dogancanbakir I cannot commit code directly to the repository, but I can provide you with the complete code changes to apply. Here are the necessary modifications:

1. Update the imports section (add encoding/json and html):

import (
	"bytes"
	"encoding/json"
	"fmt"
	"html"
	"net/http"
	"strings"

	"github.com/containrrr/shoutrrr"
	"github.com/pkg/errors"
	"go.uber.org/multierr"

	"github.com/projectdiscovery/gologger"
	"github.com/projectdiscovery/notify/pkg/utils"
	sliceutil "github.com/projectdiscovery/utils/slice"
)

2. Replace the workflows section (lines 68-71 in the current code):

// New Power Automate method
} else if provider == "workflows" {
	// HTML encode message to escape special characters and replace newlines with breaks
	htmlMessage := html.EscapeString(strings.ReplaceAll(msg, "\n", "<br>"))
	
	// Create payload using proper JSON marshaling
	type teamsPayload struct {
		Text string `json:"text"`
	}
	payloadObj := teamsPayload{Text: htmlMessage}
	payloadBytes, err := json.Marshal(payloadObj)
	if err != nil {
		err = errors.Wrap(err, fmt.Sprintf("failed to create JSON payload for teams notification id: %s", pr.ID))
		TeamsErr = multierr.Append(TeamsErr, err)
		continue
	}
	
	resp, err := http.Post(pr.TeamsWebHookURL, "application/json", bytes.NewBuffer(payloadBytes))
	if err != nil {
		err = errors.Wrap(err, fmt.Sprintf("failed to send workflow teams notification for id: %s", pr.ID))
		TeamsErr = multierr.Append(TeamsErr, err)
		continue
	}
	defer resp.Body.Close()
	if resp.StatusCode != http.StatusAccepted {
		err = errors.Errorf("failed to send workflow teams notification for id: %s, got status code: %d", pr.ID, resp.StatusCode)
		TeamsErr = multierr.Append(TeamsErr, err)
		continue
	}
}

These changes will:

  • Properly escape HTML special characters to prevent injection issues
  • Use JSON marshaling to prevent malformed JSON from quotes or special characters in messages
  • Handle JSON marshaling errors appropriately

You can apply these changes by updating the pkg/providers/teams/teams.go file accordingly.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟠 JSON Injection in Teams Workflow Payload Construction (CWE-91) — The JSON payload for Power Automate workflows is constructed using fmt.Sprintf without escaping user-controlled message content. An attacker can inject double quotes and control characters to break out of the JSON string value and inject arbitrary JSON fields or malformed data.

Attack Example
Input message: `test", "malicious": "injected` results in payload: `{"text": "test", "malicious": "injected"}` allowing arbitrary JSON field injection. Input with newlines and quotes can completely break the JSON structure.
Suggested Fix
Use encoding/json package to properly marshal the payload. Replace lines 69-70 with:

type WorkflowPayload struct {
    Text string `json:"text"`
}
htmlMessage := strings.ReplaceAll(msg, "\n", "<br>")
payload, err := json.Marshal(WorkflowPayload{Text: htmlMessage})
if err != nil {
    // handle error
}
resp, err := http.Post(pr.TeamsWebHookURL, "application/json", bytes.NewBuffer(payload))
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/providers/teams/teams.go` at lines 69-70, the JSON payload is
constructed using fmt.Sprintf without escaping the htmlMessage variable,
allowing JSON injection. Replace this with proper JSON marshaling: define a
struct `type WorkflowPayload struct { Text string \`json:"text"\` }`, then use
`payload, err := json.Marshal(WorkflowPayload{Text: htmlMessage})` and handle
the error before passing `payload` to `bytes.NewBuffer()` in the http.Post call
on line 71.

resp, err := http.Post(pr.TeamsWebHookURL, "application/json", bytes.NewBuffer([]byte(payload)))
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to send workflow teams notification for id: %s", pr.ID))
TeamsErr = multierr.Append(TeamsErr, err)
continue
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
err = errors.Errorf("failed to send workflow teams notification for id: %s, got status code: %d", pr.ID, resp.StatusCode)
TeamsErr = multierr.Append(TeamsErr, err)
continue
}
}
gologger.Verbose().Msgf("teams notification sent for id: %s", pr.ID)
}
Expand Down