From 8476a65aa0f73fd1e1d9451001b9d5f6b0b2428c Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Wed, 28 Oct 2026 20:21:19 +0100 Subject: [PATCH] fix(bonfire): replace invalid UTF-8 before the control scan in Sanitize (IN-08, quick-261028-rqd) --- modules/bonfire/README.md | 2 +- modules/bonfire/errors.go | 18 ++++++++++++------ modules/bonfire/errors_test.go | 13 +++++++++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/modules/bonfire/README.md b/modules/bonfire/README.md index b473bccf..8799893f 100644 --- a/modules/bonfire/README.md +++ b/modules/bonfire/README.md @@ -140,7 +140,7 @@ if catalog.Has("blog:import") { | `bonfire.WithHint` | Wraps an error in a `bonfire.HintError`; `{bin}` in the Try text becomes `bin/` or `summer`. | | `bonfire.ErrReported` | Marks an error whose details the command already printed. | | `bonfire.Reported` | Wraps an error so it matches `bonfire.ErrReported`: exit code 1, or the code of `bonfire.WithExitCode`, and no error block. | -| `bonfire.Sanitize` | Removes control characters other than tab (line breaks become spaces), so untrusted text cannot move the cursor or retitle the terminal; error blocks, process views, failure lines, tables and diary's console use it. | +| `bonfire.Sanitize` | Replaces invalid UTF-8 with U+FFFD and removes control characters other than tab (line breaks become spaces), so untrusted text, including a raw 8-bit C1 byte, cannot move the cursor or retitle the terminal; error blocks, process views, failure lines, tables and diary's console use it. | | `bonfire.WithExitCode` | Makes an error carry the exit code `bonfire.Execute` returns for it (1 to 255; other codes give 1). | | `bonfire.Listing` | The JSON catalog of `list --format=json`: `Application` (`bonfire.ApplicationInfo`), `GlobalOptions` and `Commands`. | | `bonfire.ApplicationInfo` | Name, version and framework version of a binary in a `bonfire.Listing`. | diff --git a/modules/bonfire/errors.go b/modules/bonfire/errors.go index a59982f3..f43316b3 100644 --- a/modules/bonfire/errors.go +++ b/modules/bonfire/errors.go @@ -7,6 +7,7 @@ import ( "path/filepath" "strings" "unicode" + "unicode/utf8" "git.golem15.com/golem15/summercms/internal/argmask" "git.golem15.com/golem15/summercms/modules/sunscreen" @@ -102,18 +103,23 @@ func Reported(err error) error { // text, names an administrator typed) then cannot move the cursor, clear // the screen or set the window title. Error blocks, process views, failure // lines and tables pass their text through it, and so does diary's console -// channel. A clean string is returned unchanged. +// channel. Invalid UTF-8 becomes U+FFFD first, so a raw 8-bit C1 byte (such +// as 0x9b, CSI) cannot reach a terminal that honours it, and the result is +// always valid UTF-8. A clean, valid string is returned unchanged. func Sanitize(s string) string { - clean := true - for _, r := range s { - if r != '\t' && unicode.IsControl(r) { - clean = false - break + clean := utf8.ValidString(s) + if clean { + for _, r := range s { + if r != '\t' && unicode.IsControl(r) { + clean = false + break + } } } if clean { return s } + s = strings.ToValidUTF8(s, "\uFFFD") var b strings.Builder for _, r := range s { switch { diff --git a/modules/bonfire/errors_test.go b/modules/bonfire/errors_test.go index b9457a84..ccdce45e 100644 --- a/modules/bonfire/errors_test.go +++ b/modules/bonfire/errors_test.go @@ -9,6 +9,7 @@ import ( "path/filepath" "strings" "testing" + "unicode/utf8" ) // TestExitCodes: Execute returns 0 on success, 1 on a failure and 2 on an @@ -323,9 +324,21 @@ func TestSanitize(t *testing.T) { "a\x1b]0;title\x07b": "a]0;titleb", "nul\x00 and del\x7f and \x01": "nul and del and ", "boom\x1b[2Jclear": "boom[2Jclear", + // IN-08: invalid UTF-8 becomes U+FFFD, so a raw 8-bit C1 byte + // such as 0x9b (CSI) never reaches the terminal. + "a\x9b31mb": "a\uFFFD31mb", + "\x9b": "\uFFFD", + "\xffok": "\uFFFDok", } { if got := Sanitize(in); got != want { t.Errorf("Sanitize(%q) = %q, want %q", in, got, want) } + if got := Sanitize(in); !utf8.ValidString(got) { + t.Errorf("Sanitize(%q) = %q is not valid UTF-8", in, got) + } + } + clean := "plain ok" + if got := Sanitize(clean); got != clean { + t.Errorf("Sanitize(%q) = %q, want it unchanged", clean, got) } }