How to decode Base64 safely
August 12, 2026 · 15 min read
Decoding Base64 looks harmless until untrusted input crashes your service, blows memory limits, or injects control bytes into downstream systems. Safe decoding means validating charset and length, choosing the right variant (standard vs URL), and never treating decoded bytes as trusted text without further checks.
Security reviews often flag “we Base64-decode user input” without limits. This guide covers practical guardrails for APIs, browser tools, and batch jobs.
What can go wrong
A small encoded string can expand dramatically - attackers abuse that ratio for denial of service. Invalid characters may throw exceptions that become 500 errors if uncaught. Decoded binary pasted into SQL, shells, or HTML without escaping enables injection in the next hop, not in Base64 itself.
- Expansion bombs - megabyte strings that decode to gigabytes.
- Charset confusion - URL-safe tokens decoded with the standard alphabet.
- Secret leakage - decoding tokens in server logs that retain payloads.
Validate before decode
Reject input that is not a multiple of four after padding normalization, or cap maximum string length before decode. Allow only the expected alphabet: A-Za-z0-9+/= for standard, or include -_ for URL-safe.
const STANDARD_B64 = /^[A-Za-z0-9+/]*={0,2}$/;
const MAX_LEN = 1_000_000; // tune per use case
function safeDecode(b64) {
if (b64.length > MAX_LEN) throw new Error("Input too large");
if (!STANDARD_B64.test(b64)) throw new Error("Invalid charset");
return Buffer.from(b64, "base64");
}
UTF-8 and binary handling
Browser atob() returns a “binary string” of Latin-1 code units, not UTF-8 text. For Unicode, decode to bytes first, then run TextDecoder with utf-8. For images and ciphertext, keep data as Uint8Array and never coerce to string.
function base64ToUtf8(b64) {
const binary = atob(b64);
const bytes = Uint8Array.from(binary, (c) => c.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
Decoding untrusted input
Treat decoded output as untrusted: scan for null bytes if the consumer expects text, enforce size limits after decode, and avoid echoing binary back to clients as interpreted HTML. For user-uploaded “files” in Base64, re-encode server-side only after virus scanning or format validation.
Secrets should be decoded in memory-isolated paths with no logging. If you only need to inspect JWT claims, decode the payload segment without verifying signature only in dev - production must always verify first.
Local vs server decode
Developer tools that decode locally in the browser keep tokens and PII off your servers - a meaningful privacy win. Server-side decode is required for automation, but minimize retention and redact logs.
Prefer maintained libraries over hand-rolled parsers; they handle padding edge cases and constant-time comparisons where needed for crypto formats.
FAQ
- Is it safe to decode JWTs client-side?
- Decoding for display is fine; trusting claims without signature verification is not. Never make authorization decisions on decode alone.
- What maximum size should I allow?
- Depends on use case. APIs often cap at 1–10 MB encoded; thumbnails need far less. Always cap decoded size too.
- Should I log decoded Base64?
- Avoid logging decoded secrets or PII. If you must debug, use redacted previews and short TTL logs.
- Why does atob throw InvalidCharacterError?
- Illegal characters, wrong alphabet, or incorrect padding. Normalize URL-safe input and fix length mod 4 first.
Related: Base64URL vs Base64