fix(scan): truncate shodan banners on rune boundaries (#326)

truncateBanner sliced by byte offset, so a multibyte utf-8 rune landing
across the limit was cut mid-sequence and emitted invalid utf-8. shodan
banners routinely carry non-ascii bytes, so this corrupted logged and
printed output. count runes instead so the cut lands on a boundary.
This commit is contained in:
Tigah
2026-07-22 12:52:55 -07:00
committed by GitHub
parent 41f35acd66
commit a35f3bc71f
2 changed files with 9 additions and 2 deletions
+2 -2
View File
@@ -260,8 +260,8 @@ func truncateBanner(banner string, maxLen int) string {
banner = strings.ReplaceAll(banner, "\r\n", " ")
banner = strings.ReplaceAll(banner, "\n", " ")
if len(banner) > maxLen {
return banner[:maxLen] + "..."
if runes := []rune(banner); len(runes) > maxLen {
return string(runes[:maxLen]) + "..."
}
return banner
}
+7
View File
@@ -18,6 +18,7 @@ import (
"net/http/httptest"
"testing"
"time"
"unicode/utf8"
)
func TestResolveHostname_IP(t *testing.T) {
@@ -50,6 +51,9 @@ func TestTruncateBanner(t *testing.T) {
{"this is a long banner", 10, "this is a ..."},
{"with\nnewlines\r\n", 50, "with newlines"},
{" trimmed ", 50, "trimmed"},
// truncation must land on a rune boundary, not split a multibyte rune
{"aaaé", 4, "aaaé"},
{"日本語テスト", 3, "日本語..."},
}
for _, tt := range tests {
@@ -57,6 +61,9 @@ func TestTruncateBanner(t *testing.T) {
if result != tt.expected {
t.Errorf("truncateBanner(%q, %d) = %q, want %q", tt.input, tt.maxLen, result, tt.expected)
}
if !utf8.ValidString(result) {
t.Errorf("truncateBanner(%q, %d) produced invalid UTF-8: %q", tt.input, tt.maxLen, result)
}
}
}