fix: truncate strings on character boundaries (#40267)

Byte slicing like `t.Description[:255]` can split a multi-byte character
and store invalid UTF-8. Team descriptions were also cut to 255 bytes
although their `MaxSize(255)` validation allows 255 characters.

Team descriptions now truncate by runes. Signup user agents, Actions log
lines and access log request IDs use `util.TruncateStringBytes`, which
now returns short strings untouched.

---------

Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
silverwind
2026-11-07 11:15:28 +00:00
committed by GitHub
parent 5d9bfa1e14
commit baccc2d7cd
6 changed files with 28 additions and 18 deletions

View File

@@ -837,10 +837,7 @@ func createUser(ctx context.Context, u *User, meta *Meta, createdByAdmin bool, o
}
// trim user agent string to a reasonable length, if necessary
userAgent := strings.TrimSpace(meta.InitialUserAgent)
if len(userAgent) > 255 {
userAgent = userAgent[:255]
}
userAgent := util.TruncateRunes(strings.TrimSpace(meta.InitialUserAgent), 255)
if err = SetUserSetting(ctx, u.ID, SignupUserAgent, userAgent); err != nil {
return err
}

View File

@@ -16,6 +16,7 @@ import (
"gitea.dev/models/dbfs"
"gitea.dev/modules/log"
"gitea.dev/modules/storage"
"gitea.dev/modules/util"
"gitea.dev/modules/zstd"
"google.golang.org/protobuf/types/known/timestamppb"
@@ -213,10 +214,7 @@ func OpenLogs(ctx context.Context, inStorage bool, filename string) (io.ReadSeek
func FormatLog(timestamp time.Time, content string) string {
// Content shouldn't contain new line, it will break log indexes, other control chars are safe.
content = strings.ReplaceAll(content, "\n", `\n`)
if len(content) > MaxLineSize {
content = content[:MaxLineSize]
}
return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), content)
return fmt.Sprintf("%s %s", timestamp.UTC().Format(timeFormat), util.TruncateStringBytes(content, MaxLineSize))
}
func ParseLog(in string) (time.Time, string, error) {

View File

@@ -125,22 +125,29 @@ func EllipsisTruncateRunes(str string, limit int) (left, right string) {
// TruncateRunes returns a truncated string with given rune limit,
// it returns input string if its rune length doesn't exceed the limit.
func TruncateRunes(str string, limit int) string {
if utf8.RuneCountInString(str) < limit {
if len(str) <= limit {
return str
}
return string([]rune(str)[:limit])
for i := range str {
limit--
if limit < 0 {
return str[:i]
}
}
return str
}
// TruncateStringBytes returns a truncated string with given byte limit,
// it returns input string if its byte length doesn't exceed the limit.
func TruncateStringBytes(str string, limit int) string {
l := 0
if len(str) <= limit {
return str
}
for i, r := range str {
rl := utf8.RuneLen(r)
if l+rl > limit {
limit -= utf8.RuneLen(r)
if limit < 0 {
return str[:i]
}
l += rl
}
return str
}

View File

@@ -129,3 +129,12 @@ func TestTruncateRunes(t *testing.T) {
assert.Equal(t, "测试", TruncateRunes("测试", 2))
assert.Equal(t, "测试", TruncateRunes("测试", 3))
}
func TestTruncateStringBytes(t *testing.T) {
assert.Equal(t, "a", TruncateStringBytes("ab", 1))
assert.Equal(t, "ab", TruncateStringBytes("ab", 2))
assert.Empty(t, TruncateStringBytes("测试", 2))
assert.Equal(t, "测", TruncateStringBytes("测试", 5))
assert.Equal(t, "测试", TruncateStringBytes("测试", 6))
assert.Equal(t, "\xff\xff", TruncateStringBytes("\xff\xff", 2))
}

View File

@@ -15,6 +15,7 @@ import (
"gitea.dev/modules/httplib"
"gitea.dev/modules/log"
"gitea.dev/modules/setting"
"gitea.dev/modules/util"
"gitea.dev/modules/web/middleware"
)
@@ -60,7 +61,7 @@ func parseRequestIDFromRequestHeader(req *http.Request) string {
return "-"
}
if len(requestID) > maxRequestIDByteLength {
requestID = requestID[:maxRequestIDByteLength] + "..."
requestID = util.TruncateStringBytes(requestID, maxRequestIDByteLength) + "..."
}
return requestID
}

View File

@@ -110,9 +110,7 @@ func UpdateTeam(ctx context.Context, t *organization.Team, authChanged, includeA
return util.NewInvalidArgumentErrorf("empty team name")
}
if len(t.Description) > 255 {
t.Description = t.Description[:255]
}
t.Description = util.TruncateRunes(t.Description, 255)
if err = db.WithTx(ctx, func(ctx context.Context) error {
t.LowerName = strings.ToLower(t.Name)