"We can just Base64 it before storing it." If you review enough config files, internal dashboards, browser bundles, webhook handlers, or authentication code, you eventually see that sentence. It sounds plausible because the output looks scrambled:
c2VjcmV0IHBhc3N3b3Jk
But Base64 is not encryption. It is not password protection. It is not a safe way to hide API keys, JWT claims, user IDs, Basic Auth credentials, or private data in a URL. The same value decodes in one call:
atob("c2VjcmV0IHBhc3N3b3Jk");
// "secret password"
The practical rule is blunt: if the original value comes back without a secret key, Base64 did not protect it. It only changed the representation.
The Quick Security Test
When you see Base64 in code, ask what job it is doing:
| Base64 is being used to... | Good use? | What to check |
|---|---|---|
| Put bytes into JSON | Yes | Keep size limits reasonable |
Embed a small data: URI |
Yes | Do not inline large files blindly |
| Store ciphertext, nonce, salt, or signature bytes as text | Yes | The crypto step must happen before Base64 |
| Carry an opaque pagination cursor | Usually | Treat it as an interface detail, not access control |
| Build a Basic Auth header | Only with HTTPS | TLS protects the request; Base64 does not |
| Hide an API key in frontend code | No | Every browser receives both the value and the decoder |
| Store a password | No | Use slow password hashing |
| Hide JWT claims | No | Signed JWT payloads are readable unless encrypted as JWE |
| Hide user IDs or roles in URLs | No | Server-side authorization must still run |
If the sentence in your head is "so nobody can read it," you are not describing Base64. You are describing encryption, hashing, signing, access control, or secrets management.
What Base64 Actually Does
Base64 is a binary-to-text encoding scheme. RFC 4648 defines the common Base64 alphabet as:
A-Z a-z 0-9 + / =
The = character is padding. Base64 groups input bytes into 24-bit chunks: 3 input bytes become 4 printable characters. That is why Base64 output is about 33% larger than the raw bytes before compression.
btoa("hello");
// "aGVsbG8="
atob("aGVsbG8=");
// "hello"
The same round trip works from a terminal:
printf '%s' 'secret password' | base64
# c2VjcmV0IHBhc3N3b3Jk
printf '%s' 'c2VjcmV0IHBhc3N3b3Jk' | base64 -d
# secret password
There is no password prompt, private key, decryption step, salt, nonce, IV, tag, or work factor. Anyone holding the encoded string can reverse it.
Base64 vs Base64url
Standard Base64 is not always URL-safe because +, /, and = can be awkward in URLs, filenames, cookies, and token segments. Base64url changes the alphabet:
| Feature | Standard Base64 | Base64url |
|---|---|---|
| Characters 62 and 63 | + and / |
- and _ |
| Padding | Usually = |
Often omitted |
| Common places | MIME, PEM, Basic Auth, binary fields | JWTs, URL tokens, filenames |
| Security difference | None | None |
This matters for debugging JWTs. A JWT uses Base64url, not ordinary padded Base64:
header.payload.signature
If you paste a JWT segment into a standard Base64 decoder and it fails, the string may not be secret or encrypted. It may simply need Base64url handling:
function base64urlToBase64(part) {
let out = part.replace(/-/g, "+").replace(/_/g, "/");
while (out.length % 4 !== 0) out += "=";
return out;
}
That conversion changes the alphabet. It does not add confidentiality.
UTF-8: The Browser Trap
Browser btoa() and atob() work on binary strings. Plain ASCII examples are fine:
btoa("hello");
// "aGVsbG8="
But Unicode text needs a byte step first. This can surprise teams who test with hello and later encode customer names, Chinese text, emoji, or Arabic:
btoa("你好");
// InvalidCharacterError in browsers
Use UTF-8 bytes:
function utf8ToBase64(text) {
const bytes = new TextEncoder().encode(text);
const binary = Array.from(bytes, (b) => String.fromCharCode(b)).join("");
return btoa(binary);
}
function base64ToUtf8(base64) {
const binary = atob(base64);
const bytes = Uint8Array.from(binary, (ch) => ch.charCodeAt(0));
return new TextDecoder().decode(bytes);
}
const encoded = utf8ToBase64("你好");
base64ToUtf8(encoded);
// "你好"
The Base64 Encode & Decode tool on this site does the UTF-8 round trip in the browser, so non-ASCII text decodes as text instead of mojibake.
Why Developers Confuse Base64 With Encryption
Base64 often appears near real security:
- TLS certificates are stored in PEM text.
- JWTs use Base64url sections.
- HTTP Basic Auth uses Base64 for
username:password. - SSH public keys contain Base64-encoded key material.
- Encrypted ciphertext is often Base64-encoded before storage or transport.
That last point causes most confusion. Encrypted data may be Base64-encoded, but Base64 is not the encryption. The security came from a cryptographic operation such as AES-GCM, XChaCha20-Poly1305, RSA-OAEP, JWE, or TLS. Base64 only made the resulting bytes easy to store in JSON, email, headers, or a database column.
Use precise words in code and product copy:
encodedBackup // OK if it is only Base64
encryptedBackup // Only OK if real encryption happened
signedState // OK if an HMAC or signature protects integrity
passwordHash // OK if a password-hashing algorithm produced it
Names are not cosmetic here. A misleading name can make a future reviewer assume a security boundary exists when it does not.
Encoding vs Encryption vs Hashing vs Signing
These operations solve different problems:
| Operation | Main job | Secret needed? | Reversible? | Example |
|---|---|---|---|---|
| Encoding | Change representation | No | Yes | Base64, Base64url, URL encoding |
| Encryption | Hide plaintext | Yes | Yes, with key | AES-GCM, XChaCha20-Poly1305, JWE |
| Hashing | One-way fingerprint | No | No | SHA-256 for checksums |
| Password hashing | Slow password verification | Salt plus cost settings | No | Argon2id, bcrypt, scrypt |
| Signing / MAC | Prove integrity and origin | Yes, or key pair | No | HMAC, JWS, Ed25519 signature |
Base64 answers "can this byte sequence travel through a text channel?" It does not answer "who can read this?", "who changed this?", or "is this user allowed to access that record?"
Real Security Mistake 1: API Keys Hidden In Frontend Code
This pattern shows up in React apps, browser extensions, WordPress themes, and internal admin tools:
// The key is still shipped to every browser.
const apiKey = atob("c2stbGl2ZS1hYmMxMjM0NTY3ODk=");
fetch("https://api.example.com/report", {
headers: { Authorization: `Bearer ${apiKey}` },
});
Base64 does not help because the browser receives both the encoded value and the decoder. Anyone can open DevTools, inspect the JavaScript bundle, run the same atob() call, and copy the key.
The fix is architectural:
- Keep real secrets on the server.
- Give browser keys narrow scopes and domain restrictions.
- Proxy sensitive upstream calls through your backend.
- Rotate any secret that already shipped to users.
- Assume secret scanners can decode Base64-like strings.
Encoding a key may make code review harder. It does not make the key safer.
Real Security Mistake 2: Passwords Stored As Base64
This is damaging because it can sit quietly in a database for years:
// Reversible storage. Do not do this.
const storedPassword = btoa(password);
// Later:
const password = atob(storedPassword);
If the database leaks, every password comes back immediately. There is no cracking cost because there is nothing to crack.
Password storage should be one-way:
// Shape of the flow, using a password-hashing library.
const hash = await argon2.hash(password);
const ok = await argon2.verify(hash, candidatePassword);
Normal login systems should never need to recover the user's password. They only need to verify a candidate password against a stored hash.
Real Security Mistake 3: Basic Auth Misread As Encryption
The header looks encoded enough to fool people:
Authorization: Basic dXNlcjpwYXNzd29yZA==
It decodes to:
atob("dXNlcjpwYXNzd29yZA==");
// "user:password"
HTTP Basic Auth is acceptable only when the request is protected by HTTPS. TLS encrypts the HTTP exchange in transit. Base64 merely packs username:password into a header-friendly ASCII string.
Over plain HTTP, a network observer can read the credentials. In logs, reverse proxies, APM traces, and support dumps, a Basic Auth header should be treated like a plaintext password.
Real Security Mistake 4: JWT Payloads Treated As Private
A normal signed JWT has three Base64url sections:
header.payload.signature
The first two sections decode to JSON. The third section is the signature.
eyJzdWIiOiIxMjM0Iiwicm9sZSI6ImFkbWluIn0
Decodes to:
{
"sub": "1234",
"role": "admin"
}
That does not mean the token is fake. A signed JWT can be tamper-evident and still readable. The signature protects integrity: it tells the server whether the token was issued by a trusted party and whether the header or payload changed. It does not hide the payload.
Do not put passwords, API keys, session secrets, full credit-card numbers, medical details, or private profile data in a normal signed JWT payload. If claims need confidentiality, use JWE or avoid putting the data in the token.
Also: decoding is not verification. A developer tool can show claims; a server must still verify the signature, issuer, audience, expiration, key ID, algorithm policy, and application-specific authorization rules before trusting them.
Real Security Mistake 5: URL Parameters That Look Opaque
Legacy apps often pass state like this:
/profile?data=eyJ1c2VySWQiOjQyfQ==
That decodes to:
{
"userId": 42
}
Two problems follow. Anyone can read it, and anyone can change it then re-encode it. If /profile?data=... is the only thing deciding which user record to load, the bug is missing authorization.
Safer patterns:
- Treat decoded URL data as untrusted input.
- Re-check permissions on the server.
- Prefer a short random server-side reference when the state is sensitive.
- Use an HMAC or signature if the client must carry tamper-evident state.
- Keep private state out of URLs because URLs are copied into logs, browser history, analytics, and support screenshots.
Real Security Mistake 6: "Encrypted" Exports That Are Only Encoded
Another code-review smell is a function like this:
function exportEncryptedBackup(data) {
return btoa(JSON.stringify(data));
}
The function name says encrypted. The code says encoded.
If the export is only meant to be a portable text blob, rename it:
function exportBase64Backup(data) {
return btoa(JSON.stringify(data));
}
If users are being told the export is encrypted, use real authenticated encryption and make key handling explicit. That means a vetted crypto library, random nonces/IVs, authentication tags, secure key generation, and a key storage plan. Base64 can still wrap the ciphertext afterward.
What Base64 Is Actually Good For
Base64 is useful. The mistake is assigning it a security job.
Good uses:
- Binary data inside JSON, XML, HTML, or email.
- MIME email attachments.
- Small
data:URIs for images or fonts. - Cryptographic output formatting for ciphertext, signatures, nonces, salts, and public keys.
- JWT and JOSE compact serialization segments.
- Opaque pagination cursors where readability is a product concern, not a security boundary.
- Debugging API fields, JWT payloads, and config blobs.
The safe wording is "Base64-encoded," not "encrypted." That one word prevents a surprising number of production misunderstandings.
What To Use Instead
If you reached for Base64 because you wanted security, choose the primitive that matches the job:
| Goal | Use instead |
|---|---|
| Hide data from users or attackers | Authenticated encryption such as AES-GCM or XChaCha20-Poly1305 |
| Protect data in transit | HTTPS/TLS |
| Store passwords | Argon2id, bcrypt, or scrypt with per-password salts and tuned cost settings |
| Store API keys | Secrets manager, server-side environment, scopes, audit logs, and rotation |
| Prove a value was not changed | HMAC or digital signature |
| Keep users from editing URL state | Server-side authorization plus signed state or server-side session reference |
| Keep JWT claims private | JWE, or avoid putting private claims in the token |
| Put binary in JSON, email, or a header | Base64 is correct |
The hard part is rarely the algorithm name. It is threat modeling, key management, rotation, access control, and deciding who should be able to see the plaintext in the first place.
A Practical Code Review Checklist
Search for Base64 usage:
rg "btoa\\(|atob\\(|base64|Buffer\\.from\\(.*base64|toString\\('base64'\\)"
For each hit, classify it:
- Transport: bytes-to-text conversion for JSON, MIME, data URI, PEM, or storage. Usually fine.
- Formatting after crypto: ciphertext, nonce, signature, salt, or public key. Usually fine if the crypto is correct.
- Opaque interface: cursor, invite code, or client-carried state. Check authorization and tamper protection.
- Secret hiding: API key, password, token, credential, private claim, or user data. Not fine.
- Misleading naming: variables or UI copy say encrypted, secure, protected, secret, masked, or hidden. Fix the name or the implementation.
Then ask:
- Would a browser, mobile app, user, log collector, proxy, or support tool receive the encoded value?
- Is there a decoder next to it?
- Does a server verify permissions after decoding?
- Is the value signed if the client can edit it?
- If it is a JWT, is the signature verified before any trust decision?
- If it is a password, why is it reversible at all?
This keeps the review grounded. A PNG in a data URI is boring. A production API key in a JavaScript bundle is not.
Incident Response: If You Find Base64 "Secrets"
If you discover a Base64-encoded secret in source code, logs, a ticket, analytics, a URL, or a database export:
- Decode it locally to confirm what it is.
- Treat the decoded value as exposed.
- Rotate credentials, tokens, and keys that may have been visible.
- Remove the encoded copy from source, logs, screenshots, or tickets where possible.
- Replace the design with a server-side secret, scoped public key, signed state, real encryption, or password hash.
- Add a review rule so the same pattern does not return.
Do not spend time debating whether the Base64 string was "hard to notice." Automated scanners, browser users, and attackers can all decode it.
When You Actually Need An Encrypted Token
A standard signed JWT uses JWS: readable payload, tamper-evident signature. If you genuinely need confidentiality of claims, use JWE (JSON Web Encryption).
A compact JWE has five Base64url sections:
protected-header.encrypted-key.iv.ciphertext.authentication-tag
Those sections are still Base64url text. The confidentiality comes from the JWE encryption algorithm and the recipient's key, not from Base64url itself.
In many web apps, a simpler design is better: keep sensitive state on the server and send the browser a short random session identifier. Not every readable JWT needs to become a JWE.
Frequently Asked Questions
Is Base64 encryption?
No. Base64 is reversible encoding with no secret key. Anyone who has the string can decode it with a browser function, terminal command, online tool, or a few lines of code.
Is Base64 at least obfuscation?
Only in the weakest sense. It may hide the value from a casual glance, but it will not stop a developer, attacker, scanner, browser extension, log reader, or support tool.
Is Base64 secure for passwords or API keys?
No. Passwords need slow password hashing such as Argon2id, bcrypt, or scrypt. API keys should stay server-side or in a secrets manager with scopes, auditability, and rotation.
Can anyone read a JWT payload?
Yes, for ordinary signed JWTs. The header and payload are Base64url-encoded JSON. The signature can prove integrity after verification, but it does not hide the claims.
Why is Basic Auth Base64 if it is not secure?
Basic Auth uses Base64 to fit username:password into an HTTP header. HTTPS/TLS provides transport encryption. Without HTTPS, Basic Auth credentials are effectively cleartext on the network.
Is Base64url more secure than Base64?
No. Base64url replaces + and / with - and _, and often omits padding so the value works in URLs and JWTs. It is still reversible encoding.
What should I use instead of Base64 for encryption?
Use a vetted authenticated-encryption design such as AES-GCM, XChaCha20-Poly1305, or JWE, plus proper key generation, key storage, rotation, and access control. Do not design your own crypto format.
What is Base64 actually for?
Base64 represents bytes as text: binary fields in JSON, email attachments, data URIs, PEM files, signatures, ciphertext formatting, and URL-safe token segments. It is a wrapper, not a security boundary.
Encode and Decode in Your Browser
Need to inspect a Base64 string right now? Base64 Encode & Decode on fixjson.org lets you encode or decode text locally in your browser, including UTF-8 content. Use it to inspect JWT payloads, API response fields, or data: URIs, but do not treat decoding as verification or decryption.
Related Tools & Guides
- Base64 Encode & Decode - encode and decode Base64 locally in your browser.
- Decode Base64 Strings and JWT Payloads - practical Base64 and Base64url decoding.
- How to Decode a JWT - read claims and understand why decoding is not verification.
- Sensitive JSON and Local Tools - decide what is safe to paste into browser-based tools.
- JSON Stringify - inspect and escape nested JSON strings.
- RFC 4648: The Base64 Standard - the formal Base64 and Base64url background.
- RFC 7519: JSON Web Token - JWT structure and security considerations.
Sources
- RFC 4648 - Base64, Base64url, padding, alphabets, and security considerations.
- RFC 7617 - HTTP Basic Authentication and its Base64 credential format.
- RFC 7519 - JSON Web Token structure, trust decisions, and privacy considerations.
- RFC 7516 - JSON Web Encryption and compact JWE serialization.
- MDN btoa and MDN atob - browser Base64 primitives and UTF-8 caveats.
- OWASP Cryptographic Storage Cheat Sheet - encryption, authenticated modes, and key management.
- OWASP Password Storage Cheat Sheet - password hashing guidance.
Last reviewed July 2026.