27a9be38bc
1. Refactor err2 to use scoped loadErr variable (MINOR - sonnet-review-bot) The else-if branches are mutually exclusive, so the error variable should be scoped inside the block, not declared outside with err2. 2. Sanitize DisplayName before embedding in Markdown (MINOR - security-review-bot) Remote persona metadata is untrusted. Added sanitizeMarkdownText() to escape Markdown special characters and strip control characters. Applied to both the header title and the footer attribution. 3. Document YAML DoS mitigations (MINOR - security-review-bot) Added comprehensive comment in remote_persona.go explaining existing defenses: file size limit, file count cap, depth limit, node count cap, and alias cycle detection. These collectively mitigate billion-laughs and stack exhaustion attacks.
93 lines
3.1 KiB
Go
93 lines
3.1 KiB
Go
package review
|
|
|
|
import (
|
|
"fmt"
|
|
"regexp"
|
|
"strings"
|
|
)
|
|
|
|
// FormatMarkdown formats a ReviewResult into the markdown body for a Gitea review.
|
|
func FormatMarkdown(result *ReviewResult, reviewerName string) string {
|
|
return FormatMarkdownWithDisplay(result, reviewerName, reviewerName)
|
|
}
|
|
|
|
// GiteaEvent converts the verdict to the Gitea API event string.
|
|
func GiteaEvent(verdict string) string {
|
|
switch verdict {
|
|
case "APPROVE":
|
|
return "APPROVED"
|
|
case "REQUEST_CHANGES":
|
|
return "REQUEST_CHANGES"
|
|
default:
|
|
return "COMMENT"
|
|
}
|
|
}
|
|
|
|
// markdownSpecialChars matches characters that have special meaning in Markdown.
|
|
// We escape these to prevent untrusted input from breaking formatting.
|
|
// Uses a quoted string since raw strings can't contain backticks.
|
|
var markdownSpecialChars = regexp.MustCompile("([\\\\*_`\\[\\]()#<>|~])")
|
|
|
|
// sanitizeMarkdownText escapes special Markdown characters in untrusted text.
|
|
// This prevents markdown injection attacks where a malicious display name could
|
|
// break formatting, inject links, or create unexpected rendering.
|
|
func sanitizeMarkdownText(s string) string {
|
|
// First, remove any control characters and null bytes
|
|
cleaned := strings.Map(func(r rune) rune {
|
|
if r < 32 && r != '\t' && r != '\n' {
|
|
return -1 // drop the character
|
|
}
|
|
return r
|
|
}, s)
|
|
// Escape special Markdown characters by prepending backslash
|
|
return markdownSpecialChars.ReplaceAllString(cleaned, `\$1`)
|
|
}
|
|
|
|
// FormatMarkdownWithDisplay formats a ReviewResult with separate display name and sentinel name.
|
|
// displayName is sanitized to prevent Markdown injection from untrusted remote persona metadata.
|
|
// sentinelName is used for the cleanup sentinel comment (machine-readable, not rendered).
|
|
// If displayName is empty, sentinelName is used for both.
|
|
func FormatMarkdownWithDisplay(result *ReviewResult, displayName, sentinelName string) string {
|
|
var sb strings.Builder
|
|
|
|
// Use display name for header, or fall back to sentinel name
|
|
headerName := displayName
|
|
if headerName == "" {
|
|
headerName = sentinelName
|
|
}
|
|
|
|
if headerName != "" {
|
|
// Sanitize the header name to prevent Markdown injection
|
|
title := CapitalizeFirst(sanitizeMarkdownText(headerName))
|
|
sb.WriteString(fmt.Sprintf("# %s Review\n\n", title))
|
|
}
|
|
|
|
sb.WriteString("## Summary\n\n")
|
|
sb.WriteString(result.Summary)
|
|
sb.WriteString("\n\n")
|
|
|
|
if len(result.Findings) > 0 {
|
|
sb.WriteString("## Findings\n\n")
|
|
sb.WriteString("| # | Severity | File | Line | Finding |\n")
|
|
sb.WriteString("|---|----------|------|------|--------|\n")
|
|
|
|
for i, f := range result.Findings {
|
|
sb.WriteString(fmt.Sprintf("| %d | [%s] | `%s` | %d | %s |\n",
|
|
i+1, f.Severity, f.File, f.Line, f.Finding))
|
|
}
|
|
sb.WriteString("\n")
|
|
}
|
|
|
|
sb.WriteString("## Recommendation\n\n")
|
|
sb.WriteString(fmt.Sprintf("**%s** — %s\n", result.Verdict, result.Recommendation))
|
|
|
|
if sentinelName != "" {
|
|
// Sanitize headerName for the footer as well
|
|
sb.WriteString(fmt.Sprintf("\n---\n*Review by %s*\n", sanitizeMarkdownText(headerName)))
|
|
// Hidden sentinel for identifying this bot's reviews during cleanup
|
|
sb.WriteString(fmt.Sprintf("\n<!-- review-bot:%s -->\n", sentinelName))
|
|
}
|
|
|
|
return sb.String()
|
|
}
|