VYPR
High severityNVD Advisory· Published Jul 15, 2022· Updated Sep 17, 2024

Denial of Service (DoS)

CVE-2022-25891

Description

The package github.com/containrrr/shoutrrr/pkg/util before 0.6.0 are vulnerable to Denial of Service (DoS) via the util.PartitionMessage function. Exploiting this vulnerability is possible by sending exactly 2000, 4000, or 6000 characters messages.

AI Insight

LLM-synthesized narrative grounded in this CVE's description and references.

An off-by-one error in shoutrrr's util.PartitionMessage causes a panic when sending messages of length 2000, 4000, or 6000 characters, leading to DoS.

Root

Cause

The vulnerability resides in the util.PartitionMessage function of the shoutrrr package, which is used to split messages into chunks for services like Discord. An off-by-one error occurs when the message length is an exact multiple of the configured chunk size (2000). This leads to an index-out-of-range panic when the function attempts to access a slice beyond the message boundaries [1][2].

Exploitation

An attacker can exploit this vulnerability by sending specially crafted messages to a service that relies on PartitionMessage, such as the Discord notification service in shoutrrr. Specifically, messages with a length of exactly 2000, 4000, or 6000 characters trigger the panic [4]. No authentication or special privileges are required if the attacker can send messages to the service.

Impact

Successful exploitation results in a denial of service (DoS) condition, as the panic crashes the application or causes unexpected termination of the affected service. This can disrupt notifications or other critical functionality [1][4].

Mitigation

The issue has been fixed in shoutrrr version 0.6.0 with commit 6a27056 [3]. Users should upgrade to version 0.6.0 or later to prevent the vulnerability. No workarounds are available other than upgrading [2].

AI Insight generated on May 21, 2026. Synthesized from this CVE's description and the cited reference URLs; citations are validated against the source bundle.

Affected packages

Versions sourced from the GitHub Security Advisory.

PackageAffected versionsPatched versions
github.com/containrrr/shoutrrrGo
< 0.6.00.6.0

Affected products

2

Patches

1
6a27056f9d75

fix(discord): message size fixes (#242)

https://github.com/containrrr/shoutrrrnils måsénMay 21, 2022via ghsa
5 files changed · +160 98
  • pkg/services/discord/discord.go+14 12 modified
    @@ -4,12 +4,13 @@ import (
     	"bytes"
     	"encoding/json"
     	"fmt"
    +	"net/http"
    +	"net/url"
    +
     	"github.com/containrrr/shoutrrr/pkg/format"
     	"github.com/containrrr/shoutrrr/pkg/services/standard"
     	"github.com/containrrr/shoutrrr/pkg/types"
     	"github.com/containrrr/shoutrrr/pkg/util"
    -	"net/http"
    -	"net/url"
     )
     
     // Service providing Discord as a notification service
    @@ -32,15 +33,20 @@ const (
     )
     
     // Send a notification message to discord
    -func (service *Service) Send(message string, params *types.Params) error {
    -
    +func (service *Service) Send(message string, params *types.Params) (err error) {
     	if service.config.JSON {
     		postURL := CreateAPIURLFromConfig(service.config)
    -		return doSend([]byte(message), postURL)
    +		err = doSend([]byte(message), postURL)
    +	} else {
    +		items, omitted := CreateItemsFromPlain(message, service.config.SplitLines)
    +		err = service.sendItems(items, params, omitted)
     	}
     
    -	items, omitted := CreateItemsFromPlain(message, service.config.SplitLines)
    -	return service.sendItems(items, params, omitted)
    +	if err != nil {
    +		err = fmt.Errorf("failed to send discord notification: %v", err)
    +	}
    +
    +	return
     }
     
     // SendItems sends items with additional meta data and richer appearance
    @@ -121,9 +127,5 @@ func doSend(payload []byte, postURL string) error {
     		err = fmt.Errorf("response status code %s", res.Status)
     	}
     
    -	if err != nil {
    -		return fmt.Errorf("failed to send discord notification: %v", err)
    -	}
    -
    -	return nil
    +	return err
     }
    
  • pkg/services/discord/discord_json.go+17 8 modified
    @@ -2,16 +2,17 @@ package discord
     
     import (
     	"fmt"
    +	"time"
    +
     	"github.com/containrrr/shoutrrr/pkg/types"
     	"github.com/containrrr/shoutrrr/pkg/util"
    -	"time"
     )
     
     // WebhookPayload is the webhook endpoint payload
     type WebhookPayload struct {
    -	Embeds   []embedItem `json:"embeds"`
    -	Username string      `json:"username,omitempty"`
    -	AvatarURL string     `json:"avatar_url,omitempty"`
    +	Embeds    []embedItem `json:"embeds"`
    +	Username  string      `json:"username,omitempty"`
    +	AvatarURL string      `json:"avatar_url,omitempty"`
     }
     
     // JSON is the actual notification payload
    @@ -32,6 +33,10 @@ type embedFooter struct {
     // CreatePayloadFromItems creates a JSON payload to be sent to the discord webhook API
     func CreatePayloadFromItems(items []types.MessageItem, title string, colors [types.MessageLevelCount]uint, omitted int) (WebhookPayload, error) {
     
    +	if len(items) < 1 {
    +		return WebhookPayload{}, fmt.Errorf("message is empty")
    +	}
    +
     	metaCount := 1
     	if omitted < 1 && len(title) < 1 {
     		metaCount = 0
    @@ -65,10 +70,14 @@ func CreatePayloadFromItems(items []types.MessageItem, title string, colors [typ
     		embeds = append(embeds, ei)
     	}
     
    -	embeds[0].Title = title
    -	if omitted > 0 {
    -		embeds[0].Footer = &embedFooter{
    -			Text: fmt.Sprintf("... (%v character(s) where omitted)", omitted),
    +	// This should not happen, but it's better to leave the index check before dereferencing the array
    +	if len(embeds) > 0 {
    +		embeds[0].Title = title
    +
    +		if omitted > 0 {
    +			embeds[0].Footer = &embedFooter{
    +				Text: fmt.Sprintf("... (%v character(s) were omitted)", omitted),
    +			}
     		}
     	}
     
    
  • pkg/services/discord/discord_test.go+49 20 modified
    @@ -1,6 +1,7 @@
     package discord_test
     
     import (
    +	"fmt"
     	"log"
     	"time"
     
    @@ -115,12 +116,24 @@ var _ = Describe("the discord service", func() {
     		})
     	})
     	Describe("creating a json payload", func() {
    -		//When("given a blank message", func() {
    -		//	It("should return an error", func() {
    -		//		_, err := CreatePayloadFromItems("", false)
    -		//		Expect(err).To(HaveOccurred())
    -		//	})
    -		//})
    +		When("given a blank message", func() {
    +			When("split lines is enabled", func() {
    +				It("should return an error", func() {
    +					items, omitted := CreateItemsFromPlain("", true)
    +					Expect(items).To(BeEmpty())
    +					_, err := CreatePayloadFromItems(items, "title", dummyColors, omitted)
    +					Expect(err).To(HaveOccurred())
    +				})
    +			})
    +			When("split lines is disabled", func() {
    +				It("should return an error", func() {
    +					items, omitted := CreateItemsFromPlain("", false)
    +					Expect(items).To(BeEmpty())
    +					_, err := CreatePayloadFromItems(items, "title", dummyColors, omitted)
    +					Expect(err).To(HaveOccurred())
    +				})
    +			})
    +		})
     		When("given a message that exceeds the max length", func() {
     			It("should return a payload with chunked messages", func() {
     
    @@ -204,29 +217,45 @@ var _ = Describe("the discord service", func() {
     	})
     
     	Describe("sending the payload", func() {
    -		var err error
    +		var dummyConfig = Config{
    +			WebhookID: "1",
    +			Token:     "dummyToken",
    +		}
    +		var service Service
     		BeforeEach(func() {
     			httpmock.Activate()
    +			service = Service{}
    +			if err := service.Initialize(dummyConfig.GetURL(), logger); err != nil {
    +				panic(fmt.Errorf("service initialization failed: %v", err))
    +			}
     		})
     		AfterEach(func() {
     			httpmock.DeactivateAndReset()
     		})
     		It("should not report an error if the server accepts the payload", func() {
    -			config := Config{
    -				WebhookID: "1",
    -				Token:     "dummyToken",
    -			}
    -			serviceURL := config.GetURL()
    -			service := Service{}
    -			err = service.Initialize(serviceURL, logger)
    -			Expect(err).NotTo(HaveOccurred())
    +			setupResponder(&dummyConfig, 204, "")
     
    -			setupResponder(&config, 204, "")
    -
    -			err = service.Send("Message", nil)
    -			Expect(err).NotTo(HaveOccurred())
    +			Expect(service.Send("Message", nil)).To(Succeed())
    +		})
    +		It("should report an error if the server response is not OK", func() {
    +			setupResponder(&dummyConfig, 400, "")
    +			Expect(service.Initialize(dummyConfig.GetURL(), logger)).To(Succeed())
    +			Expect(service.Send("Message", nil)).NotTo(Succeed())
    +		})
    +		It("should report an error if the message is empty", func() {
    +			setupResponder(&dummyConfig, 204, "")
    +			Expect(service.Initialize(dummyConfig.GetURL(), logger)).To(Succeed())
    +			Expect(service.Send("", nil)).NotTo(Succeed())
    +		})
    +		When("using a custom json payload", func() {
    +			It("should report an error if the server response is not OK", func() {
    +				config := dummyConfig
    +				config.JSON = true
    +				setupResponder(&config, 400, "")
    +				Expect(service.Initialize(config.GetURL(), logger)).To(Succeed())
    +				Expect(service.Send("Message", nil)).NotTo(Succeed())
    +			})
     		})
    -
     	})
     })
     
    
  • pkg/util/partition_message.go+10 4 modified
    @@ -18,11 +18,17 @@ func PartitionMessage(input string, limits t.MessageLimit, distance int) (items
     	maxTotal := Min(len(runes), limits.TotalChunkSize)
     	maxCount := limits.ChunkCount - 1
     
    +	if len(input) == 0 {
    +		// If the message is empty, return an empty array
    +		omitted = 0
    +		return
    +	}
    +
     	for i := 0; i < maxCount; i++ {
    -		// If no suitable split point is found, use the chunkSize
    -		chunkEnd := chunkOffset + limits.ChunkSize
    -		// ... and start next chunk directly after this one
    -		nextChunkStart := chunkEnd
    +		// If no suitable split point is found, start next chunk at chunkSize from chunk start
    +		nextChunkStart := chunkOffset + limits.ChunkSize
    +		// ... and set the chunk end to the rune before the next chunk
    +		chunkEnd := nextChunkStart - 1
     		if chunkEnd > maxTotal {
     			// The chunk is smaller than the limit, no need to search
     			chunkEnd = maxTotal
    
  • pkg/util/partition_message_test.go+70 54 modified
    @@ -1,85 +1,101 @@
     package util
     
     import (
    +	"strings"
    +
     	"github.com/containrrr/shoutrrr/pkg/types"
     	. "github.com/onsi/ginkgo"
     	. "github.com/onsi/gomega"
    -	"strings"
     )
     
     var _ = Describe("Partition Message", func() {
    -	Describe("creating a json payload", func() {
    -		limits := types.MessageLimit{
    -			ChunkSize:      2000,
    -			TotalChunkSize: 6000,
    -			ChunkCount:     10,
    -		}
    -		When("given a message that exceeds the max length", func() {
    -			When("not splitting by lines", func() {
    -				It("should return a payload with chunked messages", func() {
    +	limits := types.MessageLimit{
    +		ChunkSize:      2000,
    +		TotalChunkSize: 6000,
    +		ChunkCount:     10,
    +	}
    +	When("given a message that exceeds the max length", func() {
    +		When("not splitting by lines", func() {
    +			It("should return a payload with chunked messages", func() {
     
    -					items, _ := testPartitionMessage(42, limits, 100)
    +				items, _ := testPartitionMessage(42, limits, 100)
     
    -					Expect(len(items[0].Text)).To(Equal(1994))
    -					Expect(len(items[1].Text)).To(Equal(1999))
    -					Expect(len(items[2].Text)).To(Equal(205))
    -				})
    -				It("omit characters above total max", func() {
    -					items, _ := testPartitionMessage(62, limits, 100)
    +				Expect(len(items[0].Text)).To(Equal(1994))
    +				Expect(len(items[1].Text)).To(Equal(1999))
    +				Expect(len(items[2].Text)).To(Equal(205))
    +			})
    +			It("omit characters above total max", func() {
    +				items, _ := testPartitionMessage(62, limits, 100)
     
    -					Expect(len(items[0].Text)).To(Equal(1994))
    -					Expect(len(items[1].Text)).To(Equal(1999))
    -					Expect(len(items[2].Text)).To(Equal(1999))
    -					Expect(len(items[3].Text)).To(Equal(5))
    +				Expect(len(items[0].Text)).To(Equal(1994))
    +				Expect(len(items[1].Text)).To(Equal(1999))
    +				Expect(len(items[2].Text)).To(Equal(1999))
    +				Expect(len(items[3].Text)).To(Equal(5))
    +			})
    +			It("should handle messages with a size modulus of chunksize", func() {
    +				items, _ := testPartitionMessage(20, limits, 100)
    +				Expect(len(items[0].Text)).To(Equal(1994))
    +				Expect(len(items[1].Text)).To(Equal(5))
    +
    +				items, _ = testPartitionMessage(40, limits, 100)
    +				Expect(len(items[0].Text)).To(Equal(1994))
    +				Expect(len(items[1].Text)).To(Equal(1999))
    +				Expect(len(items[2].Text)).To(Equal(5))
    +			})
    +			When("the message is empty", func() {
    +				It("should return no items", func() {
    +					items, _ := testPartitionMessage(0, limits, 100)
    +					Expect(items).To(BeEmpty())
     				})
     			})
    -			When("splitting by lines", func() {
    -				It("should return a payload with chunked messages", func() {
    -					items, omitted := testMessageItemsFromLines(18, limits, 2)
     
    -					Expect(len(items[0].Text)).To(Equal(200))
    -					Expect(len(items[8].Text)).To(Equal(200))
    +		})
    +		When("splitting by lines", func() {
    +			It("should return a payload with chunked messages", func() {
    +				items, omitted := testMessageItemsFromLines(18, limits, 2)
     
    -					Expect(omitted).To(Equal(0))
    -				})
    -				It("omit characters above total max", func() {
    -					items, omitted := testMessageItemsFromLines(19, limits, 2)
    +				Expect(len(items[0].Text)).To(Equal(200))
    +				Expect(len(items[8].Text)).To(Equal(200))
     
    -					Expect(len(items[0].Text)).To(Equal(200))
    -					Expect(len(items[8].Text)).To(Equal(200))
    +				Expect(omitted).To(Equal(0))
    +			})
    +			It("omit characters above total max", func() {
    +				items, omitted := testMessageItemsFromLines(19, limits, 2)
     
    -					Expect(omitted).To(Equal(100))
    -				})
    -				It("should trim characters above chunk size", func() {
    -					hundreds := 42
    -					repeat := 21
    -					items, omitted := testMessageItemsFromLines(hundreds, limits, repeat)
    +				Expect(len(items[0].Text)).To(Equal(200))
    +				Expect(len(items[8].Text)).To(Equal(200))
     
    -					Expect(len(items[0].Text)).To(Equal(limits.ChunkSize))
    -					Expect(len(items[1].Text)).To(Equal(limits.ChunkSize))
    +				Expect(omitted).To(Equal(100))
    +			})
    +			It("should trim characters above chunk size", func() {
    +				hundreds := 42
    +				repeat := 21
    +				items, omitted := testMessageItemsFromLines(hundreds, limits, repeat)
     
    -					// Trimmed characters do not count towards the total omitted count
    -					Expect(omitted).To(Equal(0))
    -				})
    +				Expect(len(items[0].Text)).To(Equal(limits.ChunkSize))
    +				Expect(len(items[1].Text)).To(Equal(limits.ChunkSize))
     
    -				It("omit characters above total chunk size", func() {
    -					hundreds := 100
    -					repeat := 20
    -					items, omitted := testMessageItemsFromLines(hundreds, limits, repeat)
    +				// Trimmed characters do not count towards the total omitted count
    +				Expect(omitted).To(Equal(0))
    +			})
     
    -					Expect(len(items[0].Text)).To(Equal(limits.ChunkSize))
    -					Expect(len(items[1].Text)).To(Equal(limits.ChunkSize))
    -					Expect(len(items[2].Text)).To(Equal(limits.ChunkSize))
    +			It("omit characters above total chunk size", func() {
    +				hundreds := 100
    +				repeat := 20
    +				items, omitted := testMessageItemsFromLines(hundreds, limits, repeat)
     
    -					maxRunes := hundreds * 100
    -					expectedOmitted := maxRunes - limits.TotalChunkSize
    +				Expect(len(items[0].Text)).To(Equal(limits.ChunkSize))
    +				Expect(len(items[1].Text)).To(Equal(limits.ChunkSize))
    +				Expect(len(items[2].Text)).To(Equal(limits.ChunkSize))
     
    -					Expect(omitted).To(Equal(expectedOmitted))
    -				})
    +				maxRunes := hundreds * 100
    +				expectedOmitted := maxRunes - limits.TotalChunkSize
     
    +				Expect(omitted).To(Equal(expectedOmitted))
     			})
     
     		})
    +
     	})
     })
     
    

Vulnerability mechanics

Generated on May 9, 2026. Inputs: CWE entries + fix-commit diffs from this CVE's patches. Citations validated against bundle.

References

8

News mentions

0

No linked articles in our index yet.