From b01928711465aa4070b00a45c42244e24783ef02 Mon Sep 17 00:00:00 2001 From: xarmian Date: Fri, 28 Aug 2026 19:32:53 +0000 Subject: [PATCH] fix(server): fold keys the way encoding/json folds them (BUG-2803) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Codex round 17, first of five findings. The previous commit fixed the ASCII half of key matching and left the Unicode half, which is this bug's own pattern one more time. encoding/json matches with Unicode SIMPLE FOLDING, not lower-casing. U+017F LATIN SMALL LETTER LONG S folds to 's', so "ſchema" reaches the `schema` struct field while strings.ToLower("ſchema") is unchanged and missed the allowlist — a nested NUL under that spelling reached the handler undetected. Matching is now strings.EqualFold against each canonical key. The test carries both a lower-case fold spelling and an upper-case one alongside the ASCII cases, and keeps its control asserting an unlisted key stays caller data in any casing. The other four round-17 findings are recorded on the item rather than patched here: they are genuine layer disagreements (duplicate keys merging differently in a typed decode than in a map, a scan-failure disposition on inputs the typed decode tolerates, and unknown-field policy) whose fixes are design decisions rather than corrections, and this seat is near its context bar. Each is written up with the measurement it needs. Claude-Session: https://claude.ai/code/session_011T365kP1N9V88y15HxL4YN --- internal/server/decode_json_nul_test.go | 6 +++++- internal/server/middleware_request_text.go | 13 ++++++++++++- 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/server/decode_json_nul_test.go b/internal/server/decode_json_nul_test.go index 7392a98a2..2a356291c 100644 --- a/internal/server/decode_json_nul_test.go +++ b/internal/server/decode_json_nul_test.go @@ -1035,7 +1035,11 @@ func TestOAuthRegisterRefusesNULBody(t *testing.T) { // Measured before the fix: `fields` refused, `Fields` and `FIELDS` accepted. func TestBodyDecodesNULMatchesKeysLikeTheDecoder(t *testing.T) { inner := `{"k":"a` + escNULLiteral + `b"}` - for _, key := range []string{"fields", "Fields", "FIELDS", "fIeLdS", "Schema", "TAGS"} { + // The last two are Unicode simple-fold spellings: encoding/json matches + // them to the same struct field, and lower-casing does NOT (codex round + // 17). U+017F LONG S folds to 's'; U+212A KELVIN SIGN folds to 'k'. + for _, key := range []string{"fields", "Fields", "FIELDS", "fIeLdS", "Schema", "TAGS", + "\u017Fchema", "\u017FCHEMA"} { body := `{"title":"x","` + key + `":` + jsonEncode(t, inner) + `}` if !bodyDecodesNUL([]byte(body)) { t.Errorf("key %q reaches the same struct field as its lower-case spelling and must be "+ diff --git a/internal/server/middleware_request_text.go b/internal/server/middleware_request_text.go index 2895a3e97..813b0e9d4 100644 --- a/internal/server/middleware_request_text.go +++ b/internal/server/middleware_request_text.go @@ -497,7 +497,18 @@ func isJSONEncodedFieldKey(k string) bool { if jsonEncodedFieldKeys[k] { return true } - return jsonEncodedFieldKeys[strings.ToLower(k)] + // EqualFold, not ToLower. encoding/json matches with Unicode SIMPLE + // FOLDING, which is wider than lower-casing: U+017F LATIN SMALL LETTER + // LONG S folds to 's', so "ſchema" reaches the `schema` field while + // strings.ToLower("ſchema") is still "ſchema" and missed the allowlist + // (codex round 17). Fixing the ASCII half and leaving the Unicode half + // would have been this bug's own pattern one more time. + for canonical := range jsonEncodedFieldKeys { + if strings.EqualFold(k, canonical) { + return true + } + } + return false } // valueDecodesNUL walks a decoded request body for a string that contains a