From a35f3bc71f431046e52c7b17aa4775e49ccea1ab Mon Sep 17 00:00:00 2001 From: Tigah <88289044+TBX3D@users.noreply.github.com> Date: Wed, 22 Jul 2026 12:52:55 -0700 Subject: [PATCH] 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. --- 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 6e91c59..8d67cac 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 a152b41..cebcbe2 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) + } } }