Base64, URL Encoding and Hashing: A Developer Quick Reference
Encoding questions show up in the middle of otherwise simple tasks: a token looks unreadable, a query string breaks when it contains an ampersand, two systems disagree about a signature, or a password hash is compared incorrectly. The three ideas involved — base64 encoding, percent-encoding, and cryptographic hashing — are often confused because all of them transform text into something that looks scrambled.
They solve different problems. Understanding which one applies saves time and prevents security mistakes.
Encoding Is Not Encryption
Encoding changes representation so data can travel through a channel that expects a certain character set. Anyone can reverse it with the right decoder. Encryption, by contrast, uses a key: without the key, the original data should be unrecoverable.
This distinction matters in reviews. A secret that has been base64-encoded is still a secret in plain sight. Treating encoding as protection is a common and serious mistake.
Base64 Encoding
Base64 maps binary data onto a 65-character alphabet: A–Z, a–z, 0–9, +, /, and = for padding. It exists because many transport formats — email bodies, JSON fields, HTTP headers — are defined for text, not arbitrary bytes.
The rules come from RFC 4648. Three details are easy to get wrong:
-
Padding is significant. A properly padded string ends with
=or==. Some decoders accept missing padding; others reject it. -
URL-safe variant uses different symbols. Standard base64 uses
+and/, which have meaning in URLs. The URL-safe alphabet substitutes-and_. - Base64 is not compression. It usually makes data larger — roughly 4 characters for every 3 bytes, plus padding.
A quick example in JavaScript:
const bytes = new TextEncoder().encode("hello");
const encoded = btoa(String.fromCharCode(...bytes));
console.log(encoded); // aGVsbG8=
In Node.js, the same operation is available through a buffer:
const encoded = Buffer.from("hello", "utf8").toString("base64");
const decoded = Buffer.from(encoded, "base64").toString("utf8");
The most common production bug is decoding UTF-8 text with atob in a browser. atob returns a binary string, not a Unicode string. If the original text contains non-ASCII characters, you need to convert bytes explicitly. The MDN documentation for atob describes the limitation.
Where Base64 Appears
-
Data URLs in HTML and CSS, such as
data:image/png;base64,... - JSON Web Tokens, whose three sections are base64url-encoded (not encrypted)
-
Basic authentication headers, which encode
username:password - Email attachments, through MIME encoding
- API payloads that carry binary blobs inside JSON
In each case, the encoding is a transport convenience. Inspect the value by decoding it before assuming it is protected.
URL Encoding and Percent-Encoding
URLs have reserved characters. ? starts a query string, & separates parameters, # starts a fragment, and / separates path segments. If a value contains one of these characters, it must be encoded so the parser does not treat it as syntax.
Percent-encoding replaces an unsafe character with % followed by its hexadecimal byte value. A space becomes %20; a literal & becomes %26.
Two functions in JavaScript handle this, and they are not interchangeable:
-
encodeURIComponent()encodes a value you are placing inside a single parameter. It escapes&,=,?,/, and#. -
encodeURI()preserves reserved characters because it is meant for a complete URL. Using it on a parameter value is a common source of injection-style bugs.
const q = "tom & jerry / 100%";
console.log(encodeURIComponent(q)); // tom%20%26%20jerry%20%2F%20100%25
On the server side, most frameworks decode query parameters automatically. Double-decoding is a frequent mistake: if your framework already decoded %26 into &, decoding again can corrupt the value or, worse, reintroduce a separator that changes the meaning of the request.
Plus Signs and Form Data
HTML form submissions using application/x-www-form-urlencoded encode spaces as +, not %20. If you parse a form body with a URL parser that expects %20, a literal plus sign in user input may be interpreted as a space. Use the correct decoder for the content type, and do not mix the two conventions.
Unicode and Multibyte Characters
Percent-encoding operates on bytes, not characters. A Chinese character, an emoji, or an accented letter occupies multiple bytes in UTF-8, so it becomes a sequence of %XX values. Decoding works only if the receiver applies the same character encoding. Mismatched assumptions produce mojibake, not errors, which makes the bug harder to notice.
Hashing
A cryptographic hash takes input of any length and produces a fixed-length digest. It is designed to be one-way: you can verify that two inputs match by comparing digests, but you cannot recover the input from the digest.
Hashing is used for:
- Integrity checks, such as comparing a downloaded file against a published checksum
- Password storage, usually through a dedicated password hashing function rather than a raw hash
- Cache keys and deduplication, where a stable fingerprint is useful
- Signatures and message authentication, when combined with a secret key
Common algorithms and their status:
| Algorithm | Output | Notes |
|---|---|---|
| MD5 | 128-bit | Broken for collision resistance; use only for non-security checksums |
| SHA-1 | 160-bit | Deprecated for security use; collisions have been demonstrated |
| SHA-256 | 256-bit | Widely used and appropriate for integrity checks |
| SHA-512 | 512-bit | Longer digest; useful where the extra length matters |
A SHA-256 example in Node.js:
import { createHash } from 'crypto';
const digest = createHash('sha256').update('hello').digest('hex');
console.log(digest);
Hashing Is Not Password Hashing
A raw SHA-256 hash of a password is fast to compute, which is exactly what an attacker wants. Dedicated password functions such as Argon2, bcrypt, or scrypt are deliberately slow and memory-intensive. If you are storing passwords, use one of those with a per-user salt, not a general-purpose digest.
Comparing Hashes Safely
Comparing two digests with a normal string equality check can leak timing information in some contexts. Use a constant-time comparison function when the comparison involves a secret, such as an HMAC verification. Most standard libraries provide one.
JSON Formatting and Validation
Encoding tasks often sit next to JSON work. A base64 payload inside a JSON field, an escaped query string inside a configuration file, and a checksum stored alongside a manifest all require the JSON itself to be valid.
Two practical rules:
- Validate before formatting. A formatter that "fixes" invalid JSON can hide the real problem.
- Preserve key order only when something depends on it. JSON objects are unordered by specification, even though most parsers preserve insertion order in practice.
For large documents, minifying reduces transfer size, but keep an unminified copy for debugging. Comparing a minified response with a readable fixture is much easier than comparing two minified blobs.
A Decision Checklist
When you see an encoded-looking string, ask:
- Is it meant to be reversible by anyone? If yes, it is encoding, not encryption.
- Which alphabet? Standard base64, base64url, percent-encoding, and hex are different and not interchangeable.
- Does the receiver expect padding? Check the specification or the library default.
- Is the character encoding declared? UTF-8 is a safe default, but verify at system boundaries.
- Is this a security operation? If the goal is confidentiality, use encryption; if the goal is password storage, use a password hash; if the goal is integrity, use a strong digest or HMAC.
Tools That Help
Most of these transformations are small enough to do in a few lines of code, but a dedicated tool is faster when you are inspecting data rather than writing a program. UEncoder provides base64, URL, and HTML entity encoding, MD5/SHA family hashes, and JSON formatting in the browser, with processing kept on the local device. Other useful references include the RFC 4648 text for base64 details and the Node.js crypto documentation for hash and HMAC APIs.
FAQ
Does base64 provide any security?
No. It is reversible by anyone who recognizes the format. It hides data from casual reading, not from an attacker.
Why does my base64 string contain - and _ instead of + and /?
It is the URL-safe variant. Use a decoder configured for base64url, or convert the alphabet before decoding.
Should I use MD5 to verify file downloads?
MD5 can detect accidental corruption, but it is not reliable against deliberate collisions. Prefer SHA-256 when the checksum is security-relevant.
Why do spaces become + in some requests?
That is form-urlencoded behavior. Use %20 for general URL components and the form decoder for form bodies.
Can I decode a hash?
No. A hash is one-way by design. If you need reversibility, you need encoding or encryption instead.
Conclusion
Base64 moves bytes through text channels, percent-encoding keeps URLs parseable, and hashing produces a fixed fingerprint for verification. They look similar at a glance and behave very differently in practice. Identifying which operation produced a string — and which one the receiver expects — resolves most of the confusion before it turns into a debugging session.
Top comments (0)