Skip to content

fix(runtime): atob returns a binary string, not UTF-8-mangled high bytes (Auth.js/jose session decrypt) - #6673

Merged
proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/atob-binary-string-high-bytes
Jul 19, 2026
Merged

proggeramlug merged 1 commit into
PerryTS:mainfrom
proggeramlug:fix/atob-binary-string-high-bytes

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Jul 19, 2026 •

Copy link
Copy Markdown
Contributor

Summary

atob decoded base64 to raw bytes and passed them straight to js_string_from_bytes, which treats the buffer as UTF-8. Any decoded byte ≥ 0x80 is invalid UTF-8 on its own, so charCodeAt then mis-decoded it:

atob("+vv8yIB/")               // bytes fa fb fc c8 80 7f
// node:  length 6, codes 250,251,252,200,128,127
// perry: length 3, codes 55983,NaN,127   ← wrong

Per the WHATWG spec, atob returns a "binary string" whose i-th UTF-16 code unit equals decoded byte i (0-255). The fix maps each byte to its Latin-1 code point (U+0000..U+00FF) and encodes that as UTF-8, so charCodeAt recovers the exact byte. This is the precise inverse of btoa, which already reads char code points and rejects > 0xff.

How it was found

Verifying Auth.js credentials login on a Next.js 16 App Router app. The session token is a JWE (jose, dir / A256CBC-HS512); jose decodes its random binary IV, ciphertext, and HMAC tag through atob (the fallback taken when Uint8Array.fromBase64 is unavailable). High bytes silently corrupted, so the token that Perry encrypted and wrote correctly (Node could decrypt Perry's cookie) failed to decrypt on read (Perry could not read its own cookie) → JWTSessionError → the session was never restored, so every authenticated page redirected back to login.

With this fix the full login round-trip restores the session end-to-end (session.user populated, authenticated routes reached).

Verification

  • atob("+vv8yIB/") → 250,251,252,200,128,127; full btoa/atob round-trip over all 256 byte values; single high byte atob("gA==") → [128]; ASCII unchanged — all byte-identical to node --experimental-strip-types.
  • New gap test test_gap_atob_binary_high_bytes.ts.
  • jose's A256CBC-HS512 encrypt→decrypt round-trip (via node:crypto, the exact backend calls) now round-trips through the base64 fallback; the real Auth.js login restores the session.

Summary by CodeRabbit

  • Bug Fixes

    • Improved atob decoding for binary data containing bytes above 0x7F.
    • Ensured decoded byte values are preserved accurately in the resulting string.
    • Maintained correct behavior for ASCII and full-byte-range Base64 round trips.
  • Tests

    • Added coverage for high-byte decoding, including 0x80 and all 256 byte values.

atob decoded base64 to raw bytes and handed them straight to
js_string_from_bytes, which treats the buffer as UTF-8. Any decoded
byte >= 0x80 is invalid UTF-8 on its own, so charCodeAt then mis-decoded
it: atob("+vv8yIB/") returned char codes [55983, NaN, 127] instead of
the six bytes [250,251,252,200,128,127].

Per the WHATWG spec, atob returns a 'binary string' whose i-th UTF-16
code unit equals decoded byte i (0-255). Map each byte to its Latin-1
code point (U+0000..U+00FF) and encode that as UTF-8 so charCodeAt
recovers the exact byte. This is the precise inverse of btoa, which
already reads char code points and rejects > 0xff.

Found while verifying Auth.js credentials login on a Next.js 16 app:
jose/Auth.js decode a JWE session token's random binary IV, ciphertext,
and HMAC tag through atob (the fallback path taken when
Uint8Array.fromBase64 is unavailable). High bytes silently corrupted,
so the token failed to decrypt on read even though it encrypted and
was written correctly (Node could read Perry's cookie; Perry could
not read its own) -> JWTSessionError -> session never restored. With
this fix the full login round-trip restores the session end to end.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: cb69476b-3f94-4b6d-bff8-52bdf11c0e99

📥 Commits

Reviewing files that changed from the base of the PR and between 77fc8d9 and 9977ac4.

📒 Files selected for processing (2)
  • crates/perry-runtime/src/string/base64_codec.rs
  • test-files/test_gap_atob_binary_high_bytes.ts

📝 Walkthrough

Walkthrough

atob now converts decoded bytes into Latin-1 code points before constructing the result string. A new test verifies high-byte decoding, ASCII behavior, single-byte 0x80 handling, and round-tripping all 256 byte values.

Changes

atob binary string behavior

Layer / File(s) Summary
Latin-1 decoding and validation
crates/perry-runtime/src/string/base64_codec.rs, test-files/test_gap_atob_binary_high_bytes.ts
The atob success path maps decoded bytes to Latin-1 characters before creating the string, with tests covering high bytes, ASCII, 0x80, and all byte values.

Estimated code review effort: 2 (Simple) | ~10 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states the runtime atob fix and its impact on high-byte binary strings.
Description check ✅ Passed The description covers the change, cause, and verification, though it doesn't follow the template sections exactly.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

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