From 9a8572fb9c66a98d36d8e3b2dbeda5a663ba3150 Mon Sep 17 00:00:00 2001 From: TBX3D <88289044+TBX3D@users.noreply.github.com> Date: Fri, 3 Jul 2026 17:01:54 -0700 Subject: [PATCH] fix(scan): truncate shodan banners on rune boundaries 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. --- internal/scan/shodan.go | 4 ++-- internal/scan/shodan_test.go | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/internal/scan/shodan.go b/internal/scan/shodan.go index 6e91c595..8d67cac8 100644 --- a/internal/scan/shodan.go +++ b/internal/scan/shodan.go @@ -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 } diff --git a/internal/scan/shodan_test.go b/internal/scan/shodan_test.go index a152b418..cebcbe27 100644 --- a/internal/scan/shodan_test.go +++ b/internal/scan/shodan_test.go @@ -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) + } } }