@@ -230,7 +230,7 @@ func deadURL(t *testing.T) string {
230230 t .Helper ()
231231 srv := httptest .NewServer (http .HandlerFunc (func (http.ResponseWriter , * http.Request ) {}))
232232 u := srv .URL
233- srv .Close () // now nothing listens on that port
233+ srv .Close ()
234234 return u
235235}
236236
@@ -275,3 +275,87 @@ func TestNotifyErrorRedactsTelegramToken(t *testing.T) {
275275 }
276276}
277277
278+ // attacker-controlled finding content (a scanned target's page title, a
279+ // crawled url, a cms name) reaches the slack/discord code block verbatim. a
280+ // title that embeds a closing fence used to break out of our wrapping block
281+ // and inject live markdown (mentions, masked links) into the channel.
282+ func TestNotifyCodeBlockBreakoutNeutralized (t * testing.T ) {
283+ var c capture
284+ srv := captureServer (t , & c )
285+
286+ evil := []finding.Finding {{
287+ Target : "https://evil.test" ,
288+ Module : "probe" ,
289+ Severity : finding .SeverityHigh ,
290+ Key : "probe:x" ,
291+ Title : "```\n @everyone pwned <https://evil.test|click>\n ```" ,
292+ }}
293+ p := & discordProvider {webhook : srv .URL }
294+ if err := p .send (context .Background (), srv .Client (), evil ); err != nil {
295+ t .Fatalf ("send: %v" , err )
296+ }
297+ var payload discordPayload
298+ if err := json .Unmarshal (c .body , & payload ); err != nil {
299+ t .Fatalf ("unmarshal: %v" , err )
300+ }
301+ // a clean payload has exactly the 2 fences we added (open+close); any more
302+ // means attacker content broke out.
303+ if fences := strings .Count (payload .Content , "```" ); fences > 2 {
304+ t .Fatalf ("INJECTION: attacker content added %d extra code fences, breaking out: %q" , fences - 2 , payload .Content )
305+ }
306+ }
307+
308+ // slack resolves a bare "<...|...>" as a link/mention independent of code-block
309+ // boundaries, so the fence fix alone isn't enough for slack: the control
310+ // characters (&, <, >) must be entity-escaped too.
311+ func TestSlackEscapesControlChars (t * testing.T ) {
312+ var c capture
313+ srv := captureServer (t , & c )
314+
315+ evil := []finding.Finding {{
316+ Target : "https://evil.test" ,
317+ Module : "probe" ,
318+ Severity : finding .SeverityHigh ,
319+ Key : "probe:x" ,
320+ Title : "<https://evil.test|click> & <!everyone>" ,
321+ }}
322+ p := & slackProvider {webhook : srv .URL }
323+ if err := p .send (context .Background (), srv .Client (), evil ); err != nil {
324+ t .Fatalf ("send: %v" , err )
325+ }
326+ var payload slackPayload
327+ if err := json .Unmarshal (c .body , & payload ); err != nil {
328+ t .Fatalf ("unmarshal: %v" , err )
329+ }
330+ if strings .Contains (payload .Text , "<https://evil.test|click>" ) {
331+ t .Fatalf ("INJECTION: unescaped slack link syntax reached the payload: %q" , payload .Text )
332+ }
333+ if ! strings .Contains (payload .Text , "<https://evil.test|click>" ) || ! strings .Contains (payload .Text , "&" ) {
334+ t .Fatalf ("expected slack control chars entity-escaped, got: %q" , payload .Text )
335+ }
336+ }
337+
338+ // robustness sanity: confirm a zero http.Client.Timeout would mean an
339+ // unbounded client (not a bug in notify per se, but documents that ctx - not
340+ // Timeout - is what bounds a hung endpoint here).
341+ func TestNotifyZeroTimeoutIsUnbounded (t * testing.T ) {
342+ blocked := make (chan struct {})
343+ srv := httptest .NewServer (http .HandlerFunc (func (w http.ResponseWriter , _ * http.Request ) {
344+ <- blocked
345+ }))
346+ t .Cleanup (func () { close (blocked ); srv .Close () })
347+
348+ ctx , cancel := context .WithTimeout (context .Background (), 300 * time .Millisecond )
349+ defer cancel ()
350+ p := & slackProvider {webhook : srv .URL }
351+ done := make (chan error , 1 )
352+ go func () { done <- p .send (ctx , srv .Client (), sampleFindings ()) }()
353+ select {
354+ case err := <- done :
355+ if err == nil {
356+ t .Fatal ("expected ctx-cancel error from hung endpoint" )
357+ }
358+ case <- time .After (3 * time .Second ):
359+ t .Fatal ("send did not honor ctx cancellation on hung endpoint" )
360+ }
361+ }
0 commit comments