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