Skip to content
d.devtul.fun
中文
Regex · Password

Password Regex

A password strength pattern built from lookaheads, plus the explanation of why forcing character classes is now considered bad advice.

The pattern

^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$

Requires at least one uppercase, one lowercase, one digit, one symbol from the listed set, and a length of 8 to 64. Each rule is a zero-width lookahead that does not consume characters.

Test this pattern

Edit the text below — matching happens locally, nothing is uploaded.

Variants

Length-first, no class mandate
^.{12,64}$

NIST-aligned: just enforce a sane minimum length and stop nagging about character classes. Easier to remember, harder to brute force when long.

Unicode-letter aware
^(?=.*\p{Lu})(?=.*\p{Ll})(?=.*\d).{8,64}$

Uses Unicode property classes so accented and non-Latin letters count as letters. Needs a Unicode-aware engine (PCRE, .NET, newer Java).

The password regex you can copy

This is the classic "strong password" pattern built entirely from lookaheads:

^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$

It demands four things at once — an uppercase letter, a lowercase letter, a digit, and a symbol — and a total length between 8 and 64. The trick is that none of those four requirements is written as ordinary consumption; they are all checks that peek ahead and then let the real match happen at the end.

Why lookaheads are the right tool

A normal token in a regex consumes characters and advances the cursor. If you wrote [A-Z] at the start, the pattern would demand the first character be uppercase, which is far stricter than "has at least one uppercase somewhere." Lookaheads fix this.

  • (?=...) is a zero-width positive lookahead. It asserts that, starting at the current position, the sub-pattern can match, but it does not move the cursor or add to the match.
  • .*[A-Z] inside the lookahead means "anywhere ahead of here there is an uppercase letter." The .* does the scanning; the [A-Z] is the thing we are hunting for.
  • Because each lookahead returns to the same starting point, you can stack four of them and they all scan the whole string independently.

Only after all four assertions pass does .{8,64}$ actually consume the password and confirm the length. The cursor never moved during the checks; that is the whole point.

Why NIST tells you not to do this

This is the core argument. NIST Special Publication 800-63B, the document most banks and governments defer to, explicitly argues against mandated character composition. The reasoning:

  • Forced mixtures produce passwords like Password1! that satisfy the rule while being among the first an attacker tries. They raise user frustration without raising real entropy.
  • They cap creativity: a long passphrase of random words (correct-horse-battery-staple style) is rejected for lacking a digit, even though it is vastly stronger than P@ssw0rd.
  • NIST instead recommends a generous minimum length (8 is the floor, 12+ is better), a check against known breached passwords, and suppression of obvious patterns.

The regex above is a fine teaching example. As a production policy it is fighting the wrong battle.

Performance and the bugs people actually hit

Lookahead patterns are usually fast because the string is scanned a small constant number of times. The failures are subtler:

  • Forgetting the anchor: writing (?=.*[A-Z]) without a leading ^ lets the check start mid-string and can green-light a password that only meets rules after junk.
  • Dropping the $: without $ after .{8,64} the engine may accept a 200-character password because it matched the first 64 and stopped caring.
  • OR instead of AND: if you mistakenly join rules with | instead of stacking lookaheads, the password only needs to satisfy one branch, defeating the policy.
  • Newline surprise: .* does not cross line breaks unless the s (dotall) flag is set. A pasted password with an embedded newline can slip past a rule unexpectedly.

Common wrong patterns and the fix

Wrong patternProblemFix
^(?=.*[A-Z])$Only checks one rule, accepts anything elseStack all lookaheads before .{n,m}$
^(?=.*[A-Z])(?=.*[a-z]).{8,}$No digit or symbol requirementAdd (?=.*\d) and (?=.*[!@#$%^&*])
^[A-Za-z\d!@#]{8,}$Requires mixed classes only at position 1Use lookaheads so classes can appear anywhere
^.{8,64}$No composition at all (sometimes fine!)Accept if you follow NIST length-first advice

Strict and loose variants

You can tighten the symbol set or relax it. A stricter rule might require two symbols; a looser one might accept any non-alphanumeric via \W. Be aware that \W includes whitespace, so a trailing space would count as a "symbol" — usually not what you want. The length-first variant ^.{12,64}$ is the most defensible modern choice and the easiest for users to satisfy.

What to use instead

Move the real security work out of the regex:

  • Length first: enforce a minimum (12 is a good bar) and a sane maximum to block denial-of-service via huge inputs.
  • Breach check: reject the password if it appears in a leaked-password dataset such as the Have I Been Pwned range API.
  • Entropy estimate: for a strength meter, compute Shannon entropy or use a zxcvbn-style estimator that understands dictionary words and keyboard patterns.

A regex can still render the UI hint, but the actual gate should be length plus a breach lookup, not a character-class obstacle course.

Code: checking a breach list

import hashlib, requests

def is_breached(pw):
    h = hashlib.sha1(pw.encode()).hexdigest().upper()
    prefix, tail = h[:5], h[5:]
    resp = requests.get("https://api.pwnedpasswords.com/range/" + prefix)
    return tail in resp.text

Pair that with a simple length check and you have a policy that is both safer and less annoying than the four-lookahead monster.

Frequently asked

What is a good password regex?

For teaching, ^(?=.*[A-Z])(?=.*[a-z])(?=.*\d)(?=.*[!@#$%^&*]).{8,64}$. For production, NIST prefers length plus a breach check instead.

Why does NIST oppose character composition rules?

They produce weak-but-compliant passwords like P@ssw0rd and block strong passphrases, raising friction without raising real strength.

Why use lookaheads instead of normal tokens?

Lookaheads check "contains a class somewhere" without consuming characters, so rules can appear anywhere in the string, not just at the start.

Is a regex enough to enforce password strength?

No. Combine a length minimum with a leaked-password check; entropy and breach status matter far more than character classes.

Related regex guides

Open the full Regex Tester