Skip to content

fix(extension): refuse RAR members that differ only in case at any depth - #522

Merged
osanderson merged 1 commit into
mainfrom
fix/rar-nested-member-case
Oct 2, 2026
Merged

osanderson merged 1 commit into
mainfrom
fix/rar-nested-member-case

Conversation

@osanderson

Copy link
Copy Markdown
Collaborator

Summary

This fixes the final pre-release security review's Medium. #511 closed the RAR case-variant differential only for a detail's top-level members. Its rationale (DisallowUnknownFields leaves nested duplicates nothing to smuggle) was wrong: that check matches names case-insensitively too. Reproduced against RARRegistry.Parse:

[{"type":"payment","instructedAmount":{"value":"1000.00","currency":"EUR","VALUE":"1.00"}}]
  • Before: Parse accepted it. encoding/json, and so RARGet and the consent page, read 1.00. The issued token carries these bytes, so a case-sensitive reader (a non-Go resource server, a payment system) read 1000.00.
  • A lone variant ("ACTIONS":[...]) was accepted too. It is actions to Go and no actions to a case-sensitive reader.

Fix:

  • Depth-wide duplicates. RARRegistry.Parse and ParseGrantedRAR refuse a repeated member, compared under Unicode simple case folding, in every object at every depth (ErrDuplicateMember). type must still be spelled exactly at the top level.
  • Tag spelling. Parse and RARGet refuse a member spelled other than its field's json tag, using the new strictjson.CheckTaggedFieldCase. Untagged fields keep encoding/json's matching, so detail types without tags work exactly as before. That's why this doesn't use the all-fields CheckFieldCase.
  • internal/strictjson fold fix. It folded names with strings.ToLower, which misses the long s: encoding/json reads "iſſ" as iss, while the lowercase compare didn't flag it. FoldKey, the same fold encoding/json applies, now lives in strictjson, and both packages use it. This tightens every JOSE/metadata decode that uses strictjson.
  • Docs. ParseGrantedRAR's and ErrDuplicateMember's docs now describe the depth-wide behaviour.

This is a fix: it refuses input that two readers would read differently. Correctly spelled details, tagged or untagged, are unaffected.

Tests

  • TestRARParseRejectsNestedCaseVariants:
    • Refused:
      • value with VALUE, and value twice;
      • a repeat inside an array of objects;
      • a lone ACTIONS, and a lone nested VALUE;
      • long s in currency.
    • Accepted: exact names, and an untagged nested field in another case (as before).
  • TestGrantedRARRejectsNestedCaseVariants: ParseGrantedRAR refuses a nested repeat, and RARGet refuses a lone variant.
  • TestCheckFieldCaseFoldsAsEncodingJSONDoes: "iſſ" for iss is refused.
  • TestCheckTaggedFieldCase: a tagged variant is refused, an untagged one is allowed, and CheckFieldCase still checks every field.
  • Mutation checks: checking duplicates at the top level only, dropping the tag check at Parse or in RARGet, using ToLower instead of FoldKey, or ignoring tagged-only each fails a test.
  • Other checks:
    • go test -race across extension, internal, server, resource and client passes.
    • go test ./cmd/... passes.
    • Every demo module's tests pass.
    • FuzzRARRegistryParse and FuzzParseGrantedRAR (15 seconds each) are clean.
    • golangci-lint is clean.

🤖 Generated with Claude Code

The case-folded duplicate check from #511 looked at a detail's top-level
members only. Its rationale was that DisallowUnknownFields leaves nested
duplicates nothing to smuggle, but it matches names case-insensitively
too. So

  {"type":"payment","instructedAmount":{"value":"1000.00","VALUE":"1.00",...}}

was accepted. encoding/json, and so the consent page, read 1.00, while
a case-sensitive reader of the issued token (which carries these bytes)
read 1000.00. A lone "ACTIONS" was likewise actions to one reader and no
actions at all to the other.

- RARRegistry.Parse and ParseGrantedRAR now refuse a repeated member,
  compared under Unicode simple case folding, in every object at every
  depth, as ErrDuplicateMember.
- Parse and RARGet refuse a member spelled other than its field's json
  tag, using strictjson's new CheckTaggedFieldCase. Untagged fields keep
  encoding/json's matching, so detail types without tags work as before.

internal/strictjson compared names with strings.ToLower, which misses
the long s: encoding/json reads "iſſ" as "iss". It now uses FoldKey,
the same fold encoding/json applies, which moves here from extension.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@codecov

codecov Bot commented Oct 2, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.17647% with 6 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
internal/strictjson/strictjson.go 88.88% 2 Missing and 2 partials ⚠️
extension/rar.go 93.54% 1 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

@sonarqubecloud

sonarqubecloud Bot commented Oct 2, 2026

Copy link
Copy Markdown

@osanderson
osanderson merged commit 1b3c7c3 into main Oct 2, 2026
17 checks passed
@osanderson
osanderson deleted the fix/rar-nested-member-case branch October 2, 2026 08:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant