SHA-256 is not broken. It is fast, and speed is the problem
· 10 min read
SHA-256 is a sound hash and the wrong tool for passwords. Here is what password hashing requires, why speed is the liability, and how to migrate.

Ask what search results call sha256 password hashing and you get two confident answers. One says SHA-256 is broken. The other says SHA-256 is a sound cryptographic hash, so it is fine for passwords. Both are half right, and the honest position is narrower than either: SHA-256 is not broken, and it is still the wrong tool for storing a password. The defect is fit for purpose, not soundness. It was engineered to run fast, and against a password guessing attack, speed is the liability.
This post is about the algorithm, not about your setup. There is no scanner and no form here, because there is nothing you should be pasting anywhere. It covers what password hashing requires that a general-purpose hash does not, what a leak of a fast-hashed store would hand over, and what a reasonable migration looks like if you find you are on SHA-256.
What single-call SHA256 password hashing leaves out
The NIST SP 800-63B-4 digital identity guidelines state the requirement plainly: "Verifiers SHALL store passwords in a form that is resistant to offline attacks. Passwords SHALL be salted and hashed using a suitable password hashing scheme. Password hashing schemes take a password, a salt, and a cost factor as inputs and generate a password hash. Their purpose is to make each password guess more expensive for an attacker who has obtained a hashed password file, thereby making the cost of a guessing attack high or prohibitive."
Three inputs. A call to sha256($password) supplies one, and there is nowhere in the function signature to put the other two. You could bolt a salt on outside the hash, but the cost factor has no home at all. The computation is a fixed sequence of rounds that runs at whatever speed the hardware manages.
That is the entire gap, and it is not a tuning problem. It is a difference in what the two kinds of hash are for.
Cracking a hash is guessing, not reversing
Nobody reverses SHA-256. OWASP's Password Storage Cheat Sheet describes the procedure as selecting a password the attacker thinks the victim chose, calculating the hash, then comparing that hash to the stored one. Candidate lists come from "lists of passwords obtained from other compromised sites", from brute force, or from "dictionaries or wordlists of common passwords".
The mystique makes people misjudge this in both directions. The attacker does not need a flaw in the hash, only a fast machine and a good list. Each attempt returns one bit, match or no match, and the only thing they optimise is attempts per second.
The same source on why that rate matters: "While the number of permutations can be enormous, with high speed hardware (such as GPUs) and cloud services with many servers for rent, the cost to an attacker is relatively small to do successful password cracking, especially when best practices for hashing are not followed."
Note what that quote does not give you: a hashes-per-second figure. Any number pinned to a particular chip will date badly. The structural point does not date: on a general-purpose hash the guess rate is set by commodity hardware, and on a password hash the designer sets it deliberately.
Why SHA256 password hashing hands over a cheap guess budget
"Some modern hashing algorithms have been specifically designed to securely store passwords," the same cheat sheet notes, "This means that they should be slow (unlike algorithms such as MD5 and SHA-1, which were designed to be fast), and you can change how slow they are by changing the work factor."
Slow is a setting there, not an accident. SHA-256's speed was a deliberate decision, made for file integrity and digital signatures, where speed is the goal. Nothing about the algorithm degrades over time. What changes is that the hardware around it gets faster while the hash stays where it was.
SHA-256's preimage and collision resistance are intact. Wrong tool and broken tool are different diagnoses, and confusing them produces most of the bad advice in circulation about password storage.
Salt removes the shortcut, not the effort
A salt is "a unique, randomly generated string that is added to each password as part of the hashing process", per OWASP's sheet. Because every user gets a different one, an attacker "has to crack hashes one at a time using the respective salt rather than calculating a hash once and comparing it against every stored hash", and salting also "protects against an attacker's pre-computing hashes using rainbow tables or database-based lookups."
The line that gets missed answers the most common question about salts: "Finally, salting means that it is impossible to determine whether two users have the same password without cracking the hashes, as the different salts will result in different hashes even if the passwords are the same."
So salting does not make each guess harder. It removes the shared target: the one precomputed table, the one comparison that works against every row at once.
On length, NIST specifies that "the salt SHALL be at least 32 bits in length and chosen to minimize salt value collisions among stored hashes". Thirty-two bits is the number, not the 64 bits older write-ups tend to quote. The practical advice is simpler than the floor: use the salt your library generates.
Stretching adds the effort a salt cannot
A salt changes the input, not the cost of a guess. Add a salt to plain SHA-256 and you have removed the rainbow table while leaving per-guess speed exactly where SHA-256 put it. That half-fix ships often because the first half is visible and the second half is invisible.
Stretching is the other half, and it is an explicit parameter rather than a property you hope for. RFC 8018 defines PBKDF2 over a password, a salt, an iteration count, and a requested derived key length. The count is a standardised input you choose. PBKDF2 is SHA-2 plus a cost knob. hash($password) is SHA-2 with that knob omitted.
Argon2 does the same job differently. RFC 9106 defines its three cost parameters (total memory, number of passes, and degree of parallelism) in the standard itself rather than in a recommendation sheet, and its own suggested settings run higher than the floors below. That is a healthy sign, not a contradiction: the standard assumes you will tune.
RFC 8018 puts password authentication outside its remit: "other cryptographic techniques based on passwords, such as password-based key entity authentication and key establishment protocols are outside the scope of this document." PBKDF2 is a key-derivation specification that password storage borrows. It works, and it is the right pick when FIPS validation is required, but it was not written for this job.
What the published parameters mean and whose numbers they are
The starting point is Argon2id, which OWASP recommends because it "provides a balanced approach to resisting both side-channel and GPU-based attacks". Any one of its minimum configurations is an acceptable floor: 46 MiB with one pass, 19 MiB with two, 12 MiB with three, 9 MiB with four, or 7 MiB with five, all at parallelism one, each giving what OWASP calls "an equal level of defense".
The rest of the ladder. Scrypt when Argon2id is unavailable. Bcrypt only "in legacy systems where Argon2 and scrypt are not available", with a work factor "as large as verification server performance will allow, with a minimum of 10". Bcrypt also has a 72-byte input limit that caps password length. PBKDF2 when FIPS validation is required: 600,000 iterations for HMAC-SHA256, 220,000 for HMAC-SHA512, and 1,400,000 for HMAC-SHA1, the last legacy only.
Two corrections worth making explicitly, because both are widely misreported. Those iteration counts are OWASP's, not NIST's. SP 800-63B-4 defers to SP 800-132 and asks only for a cost "as high as practical", so quoting NIST for 600,000 is wrong even though the number is sound. And every figure above carries the source's own caveat, printed on the same page: "(Number as of December 2022, based on testing of RTX 4000 GPUs)".
OWASP's instruction is to tune rather than copy: "Benchmark the chosen parameters on the target system", and "increasing parallelism is not a guarantee of slower password guessing". A higher work factor slows your own login too, which is why these are floors rather than settings to paste and stop thinking about.
Five things that get mistaken for a fix
A strong hash is not a strong password store
Cryptographic strength and suitability are separate axes. A hash can be unbreakable and still be wrong here. What an attacker needs is not difficulty inverting the function, which they never attempt, but difficulty running it once per guess.
Salting is not stretching
Salt plus plain SHA-256 is still plain SHA-256, at the same speed, once per candidate. It removes the precomputed table and leaves the guess rate untouched, which is why the half-fix described above ships so often.
You cannot retro-fit a cost factor onto a digest you already stored
Stretching is a property of how the stored value was computed, not a flag you add afterwards, and better storage hygiene does not make a fast hash slow. The only retrofit is recomputation, and recomputation needs the plaintext, so it only happens when a user logs in. That constraint shapes the migration below.
A pepper is not a salt
NIST treats this as standard practice: verifiers "SHOULD perform an additional iteration of a keyed hashing or encryption operation using a secret key known only to the verifier", with the key stored "separately from the hashed passwords" and preferably "within a hardware-protected area, such as a hardware security module."
OWASP draws the line explicitly: "A pepper is shared between stored passwords, rather than being unique to an individual password like a password salt. Unlike a password salt, the pepper should not be public and should not be stored along with the generated hash." Implemented as a second HMAC pass before storage, its payoff is narrow but real: an attacker holding only the database cannot use it. One line is enough to keep: a salt is public and per-record, a pepper is secret and global.
Algorithm secrecy is not a defence
OWASP is unambiguous: "You do not need to hide which password hashing algorithm is used by an application." Concealing the choice buys nothing, because someone holding a table of candidate hashes identifies the function from the output shape. Spend the effort on parameters instead.
If you already store unsalted SHA-256
This is a latent risk, not an active one. The algorithm choice implies no breach and no evidence of one. The exposure is conditional on your database leaving your control. If it never does, nothing happens. If it does, a fast hash makes the file cheap to work through. That is the gap between learning some passwords and learning most of them.
Re-hash on the next successful login. OWASP's guidance is that when users authenticate, "that input should be re-hashed using the new algorithm." Your verification path reads the stored record, notices the old scheme, recomputes with the current one, and writes it back.
Writing that function is also the moment to stop comparing digests yourself. Go's crypto/subtle package states the property that motivates the habit: "The time taken is a function of the length of the slices and is independent of the contents". In a login path the attacker already holds every hash, so timing is a weak lever. Call the library's verify routine because it gets scheme detection and comparison right together, not because you are under attack.
Expire what re-hashing cannot reach. OWASP is direct about the limit: "However, this means that old (less secure) password hashes will be stored in the database until the user logs in." So defenders should also "expire the users' current password and require them to enter a new one, so that any older (less secure) hashes of their password are no longer useful to an attacker." Applied in bulk to long-inactive accounts, OWASP calls this secure but "not particularly user-friendly": a fair trade for a defined end state.
Do not layer your way out. The tempting shortcut is to wrap the old digest, so md5($password) becomes bcrypt(md5($password)) and the problem disappears without ever learning a plaintext. OWASP's verdict should be quoted unsoftened: "Layering the hashes avoids the need to know the original password; however, it can make the hashes easier to crack." The reasoning is short. bcrypt only slows guesses of the 32-character hex string you handed it, and such a value comes from a far smaller space than a real passphrase. Longer input, lower entropy. Store the new hash and discard the old one.
Record the scheme and cost factor beside each hash. NIST's rule is that "a reference to the password hashing scheme used, including the cost factor, SHOULD be stored for each password to allow migration to new algorithms and work factors." Without that field the first step above is not implementable, and every future upgrade becomes an outage rather than a background job. The same trap applies to raising a work factor.
What to check on your own systems
This post cannot tell you what your system does, and it is not going to imply otherwise. Checking is a small, bounded piece of work against your own code:
- Read the verification path, not the registration path. A
sha256(...)call inside a function that checks a submitted password is the finding. - Look at what sits beside the digest: an algorithm identifier and a cost parameter, or nothing but the hash?
- Confirm the salt is generated per user and stored with the record, rather than shared across accounts.
- If the answer names Argon2id, scrypt, bcrypt or PBKDF2 at a documented cost, this decision is already made.
None of that involves sending anything to anyone. Whether your systems have been breached is a separate question, and nothing here speaks to it.
Related Tools & Further Reading
- Passkeys vs passwords — the adjacent question: how much of this disappears if you stop storing passwords.
- What are passkeys — the same ground from the credential side.
- The ToolSura web security hub — header, cookie and transport checks that sit alongside authentication work.
- OWASP's Password Storage Cheat Sheet — the parameter tables and migration guidance, revised as hardware moves.
- NIST SP 800-63B-4 — the requirement verifiers are held to.
Written by
Abhay Khant
Abhay Khant is the founder of ToolSura, a privacy-first developer tools platform. Writes about client-side architecture, AI tooling, and the open web.