Safe Handling of JSON Serialization, UTF-8, and URL/Base64 Encoding
Data interchange across modern HTTP APIs, configuration manifests, and cryptographic tokens relies heavily on three standards: JSON (RFC 8259), UTF-8 (RFC 3629), and Base64 (RFC 4648). Subtle mismatches in how runtimes parse numbers, characters, and binary octets are a frequent source of production outages and security vulnerabilities.
1. RFC 8259 JSON Interoperability Pitfalls
While JSON syntax is intentionally minimal, RFC 8259 highlights several areas where parsers diverge or lose precision:
-
64-Bit Integer Precision Loss (IEEE-754): JavaScript and browser
JSON.parse()represent all JSON numbers as 64-bit IEEE-754 floating-point values, which only guarantee exact integer precision up toNumber.MAX_SAFE_INTEGER(253 − 1 = 9,007,199,254,740,991). A 64-bit database ID or Snowflake timestamp such as9007199254740993will silently round to9007199254740992! Always serialize 64-bit integers as JSON strings ("9007199254740993") when interoperating with browser clients. - Duplicate Object Keys: RFC 8259 states that object member names should be unique. When duplicate keys exist, some parsers keep the last key while others keep the first—enabling parser differential attacks against validation proxies.
-
Never Evaluate JSON as Code: Always parse JSON using a strict grammar parser (
JSON.parsein JavaScript,json.loadsin Python) and never pass untrusted strings toeval()ornew Function().
2. Why Browser btoa() Fails on Unicode (and How to Fix It)
Base64 maps 3-byte (24-bit) groups of binary data into 4 ASCII characters. In browser JavaScript, the legacy window.btoa() function treats each UTF-16 code unit of a DOMString as a single byte (0–255). Passing any character outside Latin-1—such as "✓" (U+2713) or Cyrillic/CJK text—throws an immediate InvalidCharacterError.
The standards-compliant solution converts the string into a UTF-8 Uint8Array via TextEncoder before Base64 encoding, and decodes back via TextDecoder('utf-8', { fatal: true }):
// UTF-8 safe encoding pipeline
const utf8Bytes = new TextEncoder().encode("UTF-8 ✓ Δ");
// Decode with { fatal: true } to reject malformed byte sequences
const text = new TextDecoder("utf-8", { fatal: true }).decode(utf8Bytes);
3. Standard Base64 vs. Base64URL (RFC 4648 §5)
Standard Base64 (RFC 4648 §4) uses + at index 62, / at index 63, and = for padding. Because +, /, and = have reserved meanings in URI paths and query strings, placing standard Base64 directly into a URL corrupts data unless percent-encoded.
| Specification | 62nd Char | 63rd Char | Padding (=) |
Primary Use Cases |
|---|---|---|---|---|
| Base64 (RFC 4648 §4) | + |
/ |
Required | PEM certificates, MIME email attachments, SRI hashes |
| Base64URL (RFC 4648 §5) | - |
_ |
Omitted / Optional | URL parameters, WebAuthn challenges, JWS/JWT segments |
4. encodeURI vs. encodeURIComponent
When constructing URLs dynamically in JavaScript:
- Use
encodeURIComponent(value)for individual query parameter keys and values. It percent-encodes reserved URI delimiters including&,=,+,/,?, and#. - Use
encodeURI(fullUrl)only when normalizing a complete URI whose structural delimiters (https://host/path?a=b) must remain intact.