Skip to content

[patch] Reject JWT segments outside the unpadded base64url alphabet - #409

Merged
matt-edmondson merged 1 commit into
mainfrom
fix/329-jwt-strict-base64url
Oct 9, 2026
Merged

matt-edmondson merged 1 commit into
mainfrom
fix/329-jwt-strict-base64url

Conversation

@matt-edmondson

Copy link
Copy Markdown
Contributor

Fixes #329

What was wrong

IsJwtTokenAttribute.DecodesToJsonObject mapped -/_ to +// and passed the result to Convert.FromBase64String. That call skips whitespace and accepts +, / and =, none of which are in the base64url alphabet (RFC 7515 §2 also forbids padding). Because JwtToken stores the value verbatim, malformed tokens were accepted and kept:

  • eyJh bGci OiJI UzI1 NiJ9.eyJzdWIiOiIxIn0.sig, which has spaces in the header
  • e30=.e30=.x, which uses standard-base64 padding

Change (Semantics.Strings.Identifiers)

  • A new IsBase64UrlAlphabet check requires every character to be A-Z, a-z, 0-9, - or _. It runs on the header and payload before decoding, and on the signature segment. The signature may still be empty for alg=none.
  • The existing length % 4 == 1 rejection stays, so the issue's "reject a segment whose length is 1 mod 4" criterion is still covered.
  • The XML docs on IsJwtTokenAttribute and JwtToken, and the package README's rules table, now say the segments are unpadded base64url.

Tests (JwtTokenTests)

  • Create_SegmentWithWhitespace_Throws: spaces in the header, spaces in the payload, and a newline at the end of the header
  • Create_SegmentWithPadding_Throws: e30=.e30=.x from the issue, a padded payload only, and a padded signature
  • Create_SegmentWithStandardBase64OrOtherCharacters_Throws: + and / in the payload, and +, / and a space in the signature
  • Create_UnpaddedBase64UrlSegments_Succeeds: a short valid token still passes
  • The existing valid and alg=none vectors are unchanged and still pass.

Verification

  • With IsJwtTokenAttribute.cs reverted, 7 of the new cases fail. The others (a payload with spaces, a header ending in \n, and +// in the payload) were already rejected by the decoder and stay in as guards. With the fix, all 22 JwtTokenTests pass.
  • The full Semantics.Test suite passes on net10.0: 1492 passed, 0 failed, 8 skipped (the same 8 are skipped on main).
  • Semantics.Strings.Identifiers builds with the local Sonar analyzers (.sonarlint/sonar-local.props) with 0 warnings. The first version used a foreach loop, which tripped S3267, so the check uses All instead.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LoEzLXruuTHCcJnXtVa49Z


Generated by Claude Code

IsJwtTokenAttribute mapped '-'/'_' to '+'/'/' and handed the segment to
Convert.FromBase64String, which skips whitespace and accepts '+', '/' and
'=' padding. Tokens such as "eyJh bGci OiJI UzI1 NiJ9.eyJzdWIiOiIxIn0.sig"
and "e30=.e30=.x" therefore validated and were stored verbatim. Every
segment, signature included, must now use only A-Z, a-z, 0-9, '-' and '_'.

Fixes #329

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LoEzLXruuTHCcJnXtVa49Z
@sonarqubecloud

sonarqubecloud Bot commented Oct 8, 2026

Copy link
Copy Markdown

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.

JwtToken accepts segments that are not base64url: embedded spaces ("eyJh bGci OiJI UzI1 NiJ9.…"), = padding and +// all validate

2 participants