fix(web): a body cut off mid-file is a 400; bidirectional controls are stripped from a title (second review)

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-02 17:22:41 +02:00
co-authored by Claude Sonnet 5.5
parent 9a2f24b3c9
commit 22c0317c71
+14 -1
View File
@@ -152,6 +152,10 @@ func (a *App) uploadFailure(w http.ResponseWriter, r *http.Request, userID strin
a.logger().Info("upload rejected", "reason", string(uploads.ReasonTooLarge)) a.logger().Info("upload rejected", "reason", string(uploads.ReasonTooLarge))
a.renderUploadsPage(w, r, userID, http.StatusRequestEntityTooLarge, a.renderUploadsPage(w, r, userID, http.StatusRequestEntityTooLarge,
"This file is larger than the upload limit.") "This file is larger than the upload limit.")
case errors.Is(err, io.ErrUnexpectedEOF):
// The sender hung up mid-file: theirs to retry, not a server fault.
a.logger().Info("upload rejected", "reason", "truncated")
a.renderUploadsPage(w, r, userID, http.StatusBadRequest, "The upload was cut off before it finished. Please try again.")
default: default:
// Not err: it can carry the file's name, a spool path or a database value. // Not err: it can carry the file's name, a spool path or a database value.
a.logger().Error("upload failed", "kind", errkind.Of(err)) a.logger().Error("upload failed", "kind", errkind.Of(err))
@@ -218,6 +222,12 @@ func titleFromFilename(name string) string {
// uuidShape is the form of an upload id. Anything else cannot be one, so it is a 404. // uuidShape is the form of an upload id. Anything else cannot be one, so it is a 404.
var uuidShape = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`) var uuidShape = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$`)
// isBidiControl reports the invisible characters that change text direction. Emoji joiners
// (ZWJ, ZWNJ) and variation selectors are not among them and are kept.
func isBidiControl(r rune) bool {
return r >= 0x202A && r <= 0x202E || r >= 0x2066 && r <= 0x2069 || r == 0x200E || r == 0x200F || r == 0x061C
}
// cleanTitle makes a title safe to store and show: invalid UTF-8 dropped, control // cleanTitle makes a title safe to store and show: invalid UTF-8 dropped, control
// characters (NUL, newlines, escapes) turned into spaces, runs of space collapsed, trimmed // characters (NUL, newlines, escapes) turned into spaces, runs of space collapsed, trimmed
// and clipped. A title is cosmetic; bad bytes in it must never fail an upload, and Postgres // and clipped. A title is cosmetic; bad bytes in it must never fail an upload, and Postgres
@@ -225,7 +235,10 @@ var uuidShape = regexp.MustCompile(`^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4
func cleanTitle(s string) string { func cleanTitle(s string) string {
s = strings.ToValidUTF8(s, "") s = strings.ToValidUTF8(s, "")
s = strings.Map(func(r rune) rune { s = strings.Map(func(r rune) rune {
if unicode.IsControl(r) { switch {
case isBidiControl(r):
return -1 // dropped: U+202E would reverse the rest of the title on its owner's page
case unicode.IsControl(r):
return ' ' return ' '
} }
return r return r