Developer Tools

Hashes and Encoding: Getting Back Into Your Own WordPress

The reset mail never arrives and you are locked out of a site you own. The way back in runs through one column: user_pass in wp_users. Here is what is actually stored there, checked line by line against wp-includes/pluggable.php, plus a browser-only tool that builds both the bare MD5 and the modern $wp bcrypt value.

Hashes and Encoding: Getting Back Into Your Own WordPress

The admin email points at a mailbox you closed two years ago. Or the host quietly stopped relaying wp_mail(). Or the reset link is sitting in a spam folder nobody owns. The “Lost your password?” form cheerfully says a message has been sent, and nothing ever lands. The site is yours, you pay the hosting invoice, and you cannot get past the login form.

The way back in runs through one column of one table: user_pass in wp_users. What sits in that column is not your password. On a current install it is a bcrypt hash carrying a three character prefix. On a site that has not had a password changed since 2015 it is a phpass hash starting $P$. You cannot type a plain password into that field and expect it to work, and you cannot read the old one back out.

Everything below was checked against wp-includes/pluggable.php on a WordPress 7.0.4 install running PHP 8.5.9. The branch order, the condition that lets an old MD5 through, the wp-sha384 key string and the bcrypt default cost were read out of that file and, where it mattered, run through the PHP binary to confirm.

One triage step first. If /wp-admin/ gives you a blank white page or a database error instead of a login form, your password is not the problem and no hash will fix it. Those are a fatal PHP error and a connection failure respectively, and they need different work. This article assumes the login form loads, rejects you, and refuses to send mail. It also assumes you own the site and can already reach its database, with the credentials sitting in wp-config.php.

The tool below builds the value you need to put in that column, and it does the general hashing and encoding work too. It runs entirely in your browser. Nothing you type or drop into it is uploaded anywhere.

Hashes and encoding

A workbench for hashes and encodings, and a way back into a WordPress site whose admin password is lost. Every digest, every conversion and every password hash is worked out here in this browser tab with crypto.subtle and code that ships with the tool. A dropped file is read from your own disk and never uploaded, there is no request to a server and no logging.

1Input

Type or paste below, or drop a file. Everything under here updates as you go.

Drop any file here
or press Enter to choose one. It is read in the browser, never uploaded.

2Hashes

MD5 and CRC32 are computed by the tool, the SHA family through crypto.subtle. Hex is lower case unless you tick the box above.

MD5
SHA-1
SHA-256
SHA-384
SHA-512
CRC32

Type a key to see the four HMAC values.

3Encoding

The same input, converted both ways. Decode reads the field above as already encoded and turns it back into text.

4WordPress password

Locked out of your own site? Turn a plain password into a value you can paste straight into the user_pass column, in two forms.

Bare MD532 characters, accepted and upgraded on next login
user_pass
SQL

wp_check_password compares the plain password against md5() whenever the stored value is 32 characters or shorter, so this logs you straight in. WordPress then replaces it with a modern hash the first time you sign in.

Modern $wp hashHMAC-SHA384, base64, then bcrypt
user_pass
SQL

The current WordPress format: the password is run through HMAC-SHA384 with the key wp-sha384, base64 encoded, then bcrypt hashed, with $wp in front so WordPress can tell it apart from a plain bcrypt hash. The salt is fresh from crypto.getRandomValues each time, so the value changes on every keystroke.

WP-CLI

WP-CLI hashes the password for you, so it takes the plain text, not either value above.

One rule. This is for regaining access to a site you own. Setting someone else's password on a server you do not control is not covered by anything here.

WunderPaint
The Dynamic Design and Automation Studio
WunderPaint is a layered image editor for your WordPress media library. WunderPaint Studio is the same thing in any browser, free and without an account.

The condition that lets a bare MD5 in

The advice you find everywhere is “paste an MD5 into user_pass and log in”. That advice is twenty years old and it is still correct, but almost nobody quotes the reason. Here is the actual opening of wp_check_password(), verbatim from wp-includes/pluggable.php:

if ( strlen( $hash ) <= 32 ) {
    // Check the hash using md5 regardless of the current hashing mechanism.
    $check = hash_equals( $hash, md5( $password ) );
} elseif ( ! empty( $wp_hasher ) ) {
    ...

Read what that test is. It is not a format check, not a prefix check, not a regular expression. It is a length check. If the stored string is 32 characters or shorter, core compares it to md5( $password ) with hash_equals() and nothing else is consulted. An MD5 digest in hex is exactly 32 characters, so it slides under the limit.

Two consequences fall out of that ordering. First, this branch runs before the $wp_hasher check, so a security plugin that swaps in its own hasher does not close this door. Second, it runs before the 4096 character password length guard further down. The MD5 path is the very first thing core tries.

An empty user_pass is also 32 characters or shorter, but it matches nothing: md5() always returns exactly 32 characters and hash_equals() compares the full strings. A blank column is unusable, not open.

Table of the four branches in wp_check_password: a 32 character MD5 compared with hash_equals, a 63 character $wp hash checked with HMAC-SHA384 then password_verify, a 34 character $P$ phpass hash run through 8192 raw MD5 rounds, and a bare 60 character bcrypt hash checked with password_verify. Every form except the $wp one is rehashed at the next successful login.

What the next login does to it

The MD5 you paste in does not stay. In wp-includes/user.php, immediately after wp_authenticate_username_password() confirms the password is valid, this runs:

if ( wp_password_needs_rehash( $user->user_pass, $user->ID ) ) {
    wp_set_password( $password, $user->ID );
}

wp_password_needs_rehash() returns true for anything that is not already prefixed with $wp while bcrypt is the active algorithm, which covers your MD5. So core takes the plaintext it just verified, hashes it properly, and writes the modern value back over your temporary one. You log in once with the weak hash and the column repairs itself. The same line sits in the email login path a little further down the file.

That is the whole trick, and it is why the MD5 route is safe enough for a recovery you perform in a couple of minutes. It is not safe as a resting state. A 32 character MD5 sitting in a live database is a password an attacker with a copy of that table cracks in seconds, so do the login straight away rather than leaving it there overnight.

There is a matching detail on the reset key. wp_signon() clears user_activation_key after a successful login, so a stale reset key left over from the mails that never arrived cleans itself up at the same moment.

The modern format, and the 72 byte wall

If you would rather write the real thing straight into the column and skip the MD5 stage, you need to reproduce what wp_hash_password() does. It is two operations, not one, and the first one exists for a reason worth knowing.

bcrypt ignores everything past the first 72 bytes of its input. That is a property of the Blowfish key schedule, not a WordPress decision, and it means a 90 character passphrase and the same passphrase with a different ending produce identical hashes. Rather than accept that, core compresses the password into a fixed size digest first, so every byte of a long passphrase influences the result.

The compression is an HMAC, not a plain hash, using SHA-384 with the literal key string wp-sha384. The comment in core calls this domain separation: the same password run through the same construction elsewhere in the codebase, or in some other application, will not produce this value, because the key differs. The arithmetic then works out neatly. SHA-384 produces 48 bytes, 48 divides evenly by 3, and base64 turns every 3 bytes into 4 characters, so the encoded digest is exactly 64 characters with no padding. Sixty-four fits under seventy-two with room to spare.

That 64 character string is what actually gets bcrypted. The result is then given a $wp prefix before it is stored, which is purely a marker: it tells wp_check_password() that the stored digest was produced from a pre-hashed password rather than from the raw one, so the verify side knows to apply the same HMAC before calling password_verify() on substr( $hash, 3 ). A stored value therefore reads $wp$2y$12$ and so on, 63 characters in total: three for the prefix and sixty for the bcrypt hash.

Diagram of the WordPress password pre-hash showing why the HMAC exists: bcrypt accepts only 72 bytes, SHA-384 produces a 48 byte digest, base64 of that digest is exactly 64 characters, and the stored $wp value is 63 characters. The code from wp_hash_password shows base64_encode of hash_hmac sha384 with the key wp-sha384, then password_hash with a $wp prefix prepended.

The WordPress section of the tool produces both forms from a plain password: the bare md5() for the fast route, and the full $wp value with a bcrypt cost you choose from 10, 11, 12 or 13. Each comes with a matching UPDATE statement built from the table prefix and user login you give it, and a WP-CLI equivalent for when you have shell access instead of phpMyAdmin. The MD5 output is always lowercase there, whatever the uppercase hex checkbox is set to, because md5() in PHP returns lowercase and hash_equals() is byte for byte.

Why $P$ hashes from 2011 still open the door

Between 2008 and WordPress 6.8, passwords were hashed with phpass, Openwall’s portable scheme. Those hashes start $P$, run 34 characters, and are stretched MD5: an 8 character salt, then a loop of raw MD5 calls over the running digest plus the password. The character at index 3 encodes the iteration count as a power of two, and WordPress used a setting that works out to 8192 rounds.

Core still honours them. wp_check_password() has an explicit $P$ branch that loads class-phpass.php and verifies through it, which is why a site restored from a decade old backup still lets its owner in. And because a $P$ hash is not $wp prefixed, the rehash line fires on that first successful login and quietly upgrades the row to bcrypt. Most sites migrated without anyone noticing, one user at a time, as people logged in.

One import trap: only $P$ is routed to phpass. The related $H$ identifier, which the phpass class itself accepts, falls through to the final password_verify() branch and fails there.

The cases that bite

Four things go wrong often enough to name.

  • Leading and trailing whitespace. wp_hash_password() calls trim( $password ) before hashing. The $wp branch of wp_check_password() does not trim. A password that begins or ends with a space is stored as the trimmed version and then compared untrimmed, so it will not match. Trim your input before you generate anything.
  • The bcrypt cost. WordPress passes no cost option, so it inherits PHP’s default. On this server PHP 8.3 reports a default cost of 10 and PHP 8.5 reports 12. Generate a cost 12 hash for a site running PHP 8.3 and password_needs_rehash() returns true, so the row is silently rewritten at the next login. Nothing breaks, but the value you carefully pasted will not be the one that stays.
  • Object caching. wp_set_password() follows its database write with clean_user_cache(). A raw SQL UPDATE does not. On a site with a persistent object cache in Redis or Memcached, the old user object can survive your change and keep rejecting the new password. Flush the cache after the update, or use the WP-CLI form instead, which goes through the proper function.
  • Corrupted paste. A hash that travelled through a chat window or a word processor can arrive with a smart quote, a non-breaking space or a stray line break inside it. It looks identical and matches nothing. If a value you are certain about refuses to work, run it through a text cleaner before you blame the hash.

There is also a limit the tool deliberately does not cross. It generates bcrypt only. If your site overrides the algorithm through the wp_hash_password_algorithm filter, which core exposes so you can select Argon2i or Argon2id where PHP offers them, the $wp value here will not be what your install would have produced. Note that the bare MD5 route still works in that case, because the length branch runs first. The tool also does not verify an existing hash against a password: bcrypt verification needs the stored salt, and reading a hash out of a live users table into a web page is not a habit worth building.

MD5 is dead for passwords and perfectly good for checksums

People hear “MD5 is broken” and conclude it should never appear anywhere. That is not quite the shape of the problem. MD5 is broken in two specific ways: it is collision-prone, meaning two different inputs can be constructed to share a digest, and it is fast, meaning a modern GPU tries billions of candidate passwords per second against it.

Speed is what kills it for passwords. A password hash is meant to be slow on purpose, and bcrypt’s cost is a power of two: cost 12 means 2^12 key schedule rounds. Timed on this server’s PHP 8.5, one bcrypt hash at cost 12 takes 0.164 seconds and at cost 10 takes 0.041 seconds, while the same PHP grinds through 200,000 MD5 digests in 0.026 seconds. That is over a million times more guesses per second, before an attacker moves off a CPU and onto a GPU. The delay is invisible when a human logs in once and ruinous when a machine works through a leaked table.

Neither weakness matters when the question is “did this file arrive intact”. Nobody is constructing a collision against your holiday photos, and you want the check to be fast. That is why MD5 and CRC32 both still appear in the tool. CRC32 is not even a cryptographic hash: it is a 32 bit error detection code from network frames and zip archives, good at catching a truncated download and useless against deliberate tampering.

Salt, HMAC, and the thing base64 is not

A salt is random data mixed into a hash so that identical passwords produce different stored values. Without one, every account using the same weak password shares the same digest, and an attacker who cracks it once has cracked all of them at no extra cost. With one, each row has to be attacked separately. bcrypt carries its salt inside the hash string itself, which is why a stored value contains the algorithm, the cost, a 22 character salt and only then the digest. The tool draws that salt from crypto.getRandomValues(), and the 22 characters it shows round-trip exactly to the 16 random bytes used, so the value it produces genuinely verifies.

Those are not the same thing as the eight salt constants in wp-config.php. AUTH_KEY and its siblings are secrets for signing login cookies and nonces. They have nothing to do with password storage, and rotating them logs everyone out without touching a single user_pass value. Two different meanings of one word, in two different files.

An HMAC is a hash with a key. Give it the same message and a different key and you get a completely different digest. It answers a question a plain hash cannot: not “what is the fingerprint of this data”, but “was this produced by someone holding the secret”. Webhook signatures are the everyday case: a service sends a payload plus an HMAC of it under a shared secret, and you recompute it to confirm the payload is really from them and arrived unmodified. WordPress uses one for a narrower job, domain separation on the password pre-hash, which is why its key is a fixed public string rather than a secret. Enter a key in the tool and the four HMAC-SHA rows appear alongside the plain digests.

Base64 is none of this. It is a way of writing arbitrary bytes using 64 printable characters, invented so that binary data could pass through systems that only handle text, such as email bodies and data URIs. It is fully reversible by anyone, with no key involved. A base64 string is not protected, it is packaged. The same goes for hex, percent-encoding and HTML entities: all four are transport formats, all four sit in the tool’s encoding panel with encode and decode directions, and none of them keeps a secret. Treat base64 as a way to hide an API key and that key is public.

Direction is the whole difference. A hash goes one way and stays there, which is why user_pass cannot be read back into a password. Encoding goes both ways by design. The decode direction in the tool is text only: with a file loaded it refuses and says so.

Getting back in, in order

The sequence is short. Open the database with the credentials from wp-config.php, find your row in wp_users by user_login, and write either the bare MD5 or the full $wp value into user_pass. The column is varchar(255), so length is never the constraint. Log in immediately: if you used the MD5, core replaces it during that same request. Then fix the mail, because it will fail again next time.

What makes this recovery rather than an attack is not the technique, which is identical either way. It is that you already hold the database credentials. Anyone with write access to wp_users could equally read your posts, drop your tables or install whatever they liked. The password column is not the boundary on that server; the credentials are. Which is why the tool ships an own-site warning next to the WordPress output.

Once you are back in, the rest of this is ordinary working equipment: a checksum for a file you have just moved, a base64 string to unpick from an inherited config, an HMAC to reproduce while chasing a webhook that keeps returning 401. Files are read with FileReader and capped at 32MB, the byte view shows the first 512 bytes, and there is no request in the page for a password to travel on.

Hashes and Encoding: Getting Back Into Your Own WordPress

Table of Contents

Learn it by building something

Every week one thing you can make the same afternoon, from dynamic templates to 3D type. Written down step by step.

One mail a week, and then it ends.
Unsubscribe in one click.

Troubleshooting

Allowed Memory Size Exhausted: Where the WordPress Memory Limit Lives

The "Allowed memory size exhausted" fatal error involves three separate ceilings, and the one most people raise is not the one that stopped the request. How PHP's memory_limit, WP_MEMORY_LIMIT and WP_MAX_MEMORY_LIMIT interact, and why images trigger the error more often than anything else.

Speed & Performance

WordPress Media Library Slow: What Actually Causes It

A media library that takes thirty seconds is not one slow thing. It is a database query, a metadata prime, a JSON build in PHP and eighty image requests, stacked on top of each other. Here is how to measure which one is hurting your site, and the fix order that actually moves the number.

Design Fundamentals

Contrast Is a Number, Not an Opinion

Contrast is the one accessibility metric that is pure arithmetic: two colours in, one number out, and the number either clears the threshold or it does not. Check a pair in the browser, then learn what the ratio measures, why relative luminance is not the average of the channels, where the large text exception really starts, and what to do when the brand colour fails.

Security & Privacy

WordPress File Permissions: 644, 755 and When Something Else

chmod 777 makes the error disappear by handing write access to every process on the server. What the three digits mean, the standard WordPress set, and why ownership decides which digit even gets read.

SEO & Structured Data

WordPress Image SEO: What Works and What Is Folklore

Most image SEO checklists are ordered by how easy each item is to write about. This one ranks the advice by mechanism instead, naming the core function behind every claim, and says plainly which parts are folklore.

Photo Editing

Brighten dark photos in your browser with a 590 KB neural network

A brightness slider adds the same amount everywhere, which is why a dark photo goes pale instead of bright. A small trained network predicts a different correction for every pixel, plus one gamma and one colour matrix for the whole frame. It runs in your browser tab and your picture never leaves it.

Download the free WunderPaint Plugin for WordPress

The WunderPaint workspace with the layers panel, adjustment sliders, text style presets and the asset library along the bottom

The Image Editor & Design Studio

Everything described here can be done in the browser, on your own site. The live demo runs the full editor with nothing to install.

Free

Chaos Art

Autonomous painters make one-of-a-kind abstract art in 3D space - gestures, art movements, painterly media, and embeds that paint a new original for every visitor.

Pro

Particle Strokes

Paint with swarms of light: twenty-two movements, a stamp you draw yourself, and curves that give a stroke a shape - the swarm keeps painting for a few seconds after you let go.

Pro

City Diorama

Any place on earth as a miniature you could hold: real streets, water and building footprints raised into a 3D diorama - or wrapped around a sphere as your own tiny planet.

Free

Papercut Art

Layered paper pictures with real depth - a photo sliced along its actual depth into up to twenty layers, parametric landscapes, animals and clouds you can shape, letters with real counters, and a look that runs on three dials.

Pro

3D Earth Studio

A hyperrealistic globe - day and night with city lights, live clouds, atmosphere halo, country borders and highlights, click-to-place markers with flight-route arcs, satellite orbits, seamless rotation video and a live website embed.

Free

Mystic Studio

Turn a birth date into wall art - a real natal chart with houses and aspects, the moon of that night, zodiac and Chinese zodiac posters, numerology cards and a synastry wheel for two, in eight artful themes.

Free

Marble Bath

Marble paper on a virtual water bath - drop, rake and comb real Ebru patterns with gestures, flowers and classic recipes, razor-sharp at any size and re-editable as a layer.

Free

Day Ring

Turn a day into a beautiful circular schedule - colour-coded time blocks as arcs around a 24-hour clock, with concentric rings for overlaps, emoji, templates and a legend.

Free

Code Shot

Turn code into a gorgeous, share-ready image - syntax highlighting, editor themes, window frames and diff highlighting - then drop it into your design as a re-editable layer.

Pro

3D Solar System Studio

Build a date-accurate 3D solar system - real planet positions for any date, photoreal textures and one slider from artistic to true scale - then drop it into your design as an editable layer.

Pro

3D Molecule Studio

Build a real 3D molecule - from a curated library, the periodic table or a SMILES string - then style it, measure it and drop it into your design as an editable layer.

Pro

3D Textile Studio

Drop your design onto cloth that behaves like the material you pick: silk falls soft, felt holds its shape, flag fabric snaps in the wind. Hang it, blow it and drape it, then lay the finished drape back into your document as a picture.

Pro

3D Particle Studio

Point the engine at any layer and it becomes a cloud of particles that keeps its colours, flowing through a sphere, a galaxy or your own outline. Keep the frame you like as a still, or embed the running engine so it keeps moving on your page.

Free

Origami

Put your own picture on the paper and watch that very sheet fold itself into a crane or a box. Every step is a station you can stop at and turn around in 3D, which is exactly where printed diagrams leave you alone.

Pro

3D Flip Studio

A hardcover you can leaf through, a limp magazine, a strewn pile of sheets, a sticker peeling off its backing. The curl is real geometry, so the print never slides across the paper.

Free

Handwriting Fonts

Draw the alphabet here or fill in a printed sheet and photograph it. What comes out is a genuine font family, installed into your site and available in every picker.

Pro

Step Guides

Turn any picture into an instruction. Every mark is pinned to a place in the image, so arrows still point at the right thing after the callout has been dragged somewhere else.

Free

Text Art

One studio, sixteen art types: ASCII and emoji art, brick, dice, cube, sticky-note, LED, ceramic and keycap mosaics, word portraits, text flows, silhouettes, element tiles and more.