A password validation regex checks a format policy; it cannot measure password strength. A predictable password such as Password1! can pass lowercase, uppercase, digit, and symbol checks. Use these patterns when you must implement an existing policy, not as proof that a password is safe.
For a new policy, NIST SP 800-63B-4 requires at least 15 characters for single-factor passwords, permits a minimum of 8 when the password is only part of MFA, and rejects mandatory character-composition rules. It recommends supporting at least 64 characters and requires screening against a blocklist of common, expected, or compromised passwords. Those requirements extend beyond regex.
Below are format-checking examples, including JavaScript, Python, and PHP. Apply the policy on the server too, never log candidate passwords, and keep password hashing and login rate limiting separate from input-format checks.
Quick reference
Minimum 8 characters, no other rules:
^.{8,}$
8+ characters with at least one lowercase, one uppercase, one digit:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$
A legacy composition-policy pattern (lower, upper, digit, special, 8+):
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$
For a length-first policy, count characters in application code and perform the server-side blocklist check. There is no single “NIST-compliant password regex”; authentication context and the rest of the verifier requirements matter.
Length-only patterns
Start simple. Length is the most important factor in password strength.
Exactly 8 chars: ^.{8}$
8 to 10 chars: ^.{8,10}$
8 or more chars: ^.{8,}$
12 or more chars: ^.{12,}$
The . matches any character except newline. To exclude whitespace as well, use ^\S{8,}$. To allow only specific characters, replace . with a character class:
Lowercase letters, 8+: ^[a-z]{8,}$
Letters (upper/lower), 8+: ^[a-zA-Z]{8,}$
Alphanumeric, 8+: ^[a-zA-Z0-9]{8,}$
These restrict the allowed characters but say nothing about which classes must appear. For that, you need lookaheads.
Lookaheads for "must contain"
A lookahead (?=...) is a zero-width assertion: it checks the rest of the pattern can be satisfied starting from the current position, without consuming any characters. Chained at the start of a pattern, lookaheads enforce "must contain" rules without restricting the order in which characters appear.
Require at least one uppercase letter:
^(?=.*[A-Z])[a-zA-Z]{8,}$
Reading left to right:
^anchors to the start.(?=.*[A-Z])is the lookahead. It scans the whole string (because.*is greedy) looking for at least one uppercase letter. If found, the lookahead passes; the engine doesn't advance.[a-zA-Z]{8,}$then matches the actual content: 8 or more letters.
Require at least one lowercase AND one uppercase:
^(?=.*[a-z])(?=.*[A-Z])[a-zA-Z]{8,}$
Two lookaheads, both must succeed.
Require lowercase, uppercase, AND digit:
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z0-9]{8,}$
Add a fourth class (special characters):
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[!@#$%^&*])[a-zA-Z0-9!@#$%^&*]{8,}$
Or accept any non-word character as "special":
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$
The . at the end is intentional: it lets the password contain any character (including Unicode), as long as the four lookahead rules pass. The uppercase and lowercase requirements here are ASCII-only. Unicode input can be accepted without satisfying those particular classes.
For more on how lookahead semantics work, see regex lookaheads and lookbehinds.
Counting required occurrences
To require N occurrences of a character class, use a counted lookahead. Require at least 3 uppercase letters:
^(?=(?:.*[A-Z]){3}).{8,}$
The (?:.*[A-Z]){3} is "any characters, then uppercase letter" repeated 3 times. This counts non-overlapping occurrences.
To require exactly 3 uppercase letters (no more), pair the positive lookahead with a negative lookahead:
^(?=(?:.*[A-Z]){3})(?!(?:.*[A-Z]){4}).{8,}$
The negative lookahead (?!(?:.*[A-Z]){4}) fails if 4 or more uppercase letters can be found. This is the trick most "exactly N" patterns use.
Require minimum 3 lowercase AND minimum 2 uppercase:
^(?=(?:.*[a-z]){3})(?=(?:.*[A-Z]){2}).{8,}$
Forbidden patterns
To reject specific weak patterns, add negative lookaheads.
Reject passwords containing the word "password":
^(?!.*password).{8,}$
Reject passwords that are entirely numeric:
^(?!^\d+$).{8,}$
Reject passwords where the same character repeats 3+ times in a row:
^(?!.*(.)\1{2}).{8,}$
The (.)\1{2} matches any character followed by the same character twice more (using a backreference). Wrapped in a negative lookahead, it rejects passwords containing aaa, 111, !!!, etc.
Examples in JavaScript, Python, and PHP
JavaScript:
const FORMAT = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^\w\s]).{8,}(?![\s\S])/u;
function matchesPasswordPolicy(input) {
return typeof input === "string" && FORMAT.test(input);
}
matchesPasswordPolicy("Password1!"); // true
matchesPasswordPolicy("password1!"); // false (no uppercase)
matchesPasswordPolicy("PASSWORD1!"); // false (no lowercase)
matchesPasswordPolicy("Password!"); // false (no digit)
matchesPasswordPolicy("Password11"); // false (no special)
matchesPasswordPolicy("Pa1!"); // false (too short)Python:
import re
FORMAT = re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$")
def matches_password_policy(value: str) -> bool:
return bool(FORMAT.fullmatch(value))
matches_password_policy("Password1!") # True
matches_password_policy("short1A!") # True (exactly 8 chars, all classes)
matches_password_policy("alllowercase1!") # False (no uppercase)PHP:
function matchesPasswordPolicy(string $value): bool {
$pattern = '/\A(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[^\w\s]).{8,}\z/u';
return preg_match($pattern, $value) === 1;
}
matchesPasswordPolicy("Password1!"); // true
matchesPasswordPolicy("nodigit!Aaaa"); // false (no digit)Engine compatibility
JavaScript, Python re, and PHP/PCRE2 support the lookaheads used above. Go's regexp and Rust's regex crate do not; check each requirement separately in those engines.
| Detail | JavaScript | Python 3 string patterns | PHP/PCRE2 |
|---|---|---|---|
| ASCII digits | [0-9] | [0-9] | [0-9] |
Meaning of \d | ASCII digits | Unicode decimal digits by default | Depends on UCP mode |
| Strict whole-string validation | Start anchor and (?![\s\S]); no m flag | re.fullmatch() | \A and \z |
| Unicode length caveat | Use u for code-point regex matching | Python strings count code points | Use UTF mode for valid UTF-8 input |
A code point is not always one displayed character. Keep normalization and length counting consistent at registration and login. Do not silently trim or truncate a password.
A Go implementation can use utf8.RuneCountInString(s) for the code-point length and separate character-class checks. len(s) counts bytes, which is different for non-ASCII input. For user-facing validation, separate checks also let you explain which policy condition failed.
Common mistakes
The bugs I've shipped or seen in code review.
Accepting only a substring. Use a whole-string check. In Python and PCRE, $ may match before a final newline; multiline mode also changes anchors. The executable examples above use strict ending checks. Do not add a JavaScript g or m flag to the validator.
Putting the character-class restriction inside the lookahead's content. A pattern like (?=[a-z]*[A-Z]) requires the uppercase letter to be after zero or more lowercase letters, which rejects a digit or symbol before the uppercase letter even if the rest of the password satisfies your policy. Use (?=.*[A-Z]) (any chars, then uppercase) so order doesn't matter.
Trying to reject Unicode by accident. A pattern that uses [a-zA-Z] rejects Über1! because Ü isn't in the class. If you want to accept Unicode, use . for the content match and let the lookaheads enforce the rules. The pattern in this article does exactly that.
Letting the regex enforce a maximum length. A maximum-length cap (^.{8,64}$) is fine, but rejecting long passwords (e.g., ^.{8,16}$) actively hurts security. NIST 800-63B recommends accepting passwords up to at least 64 characters.
Confusing format with strength or storage. A regex confirms only that the input satisfies the chosen format rules. It does nothing about storage. Hash with bcrypt, scrypt, or argon2 (never MD5 or SHA-256 alone). For application-specific guides, see the storage notes below.
Treating regex as a substitute for breach checks. A user can set a "strong-looking" password like Password1! that may appear in breach datasets. For real security, check the password against the HaveIBeenPwned k-anonymity API before accepting it.
Test cases
| Input | Strong pattern result | Reason |
|---|---|---|
Password1! | Match | All four classes present, length 10 |
P@ssword1 | Match | @ is the special character |
password1! | No match | No uppercase |
PASSWORD1! | No match | No lowercase |
Password! | No match | No digit |
Password11 | No match | No special character |
Pa1! | No match | Too short (length 4) |
Correct Horse 1! | Match | Spaces allowed by . |
Über stark 1! | No match | Ü is not in the ASCII uppercase class [A-Z] |
FAQ
One legacy composition-policy example is: ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$. It requires at least one lowercase, one uppercase, one digit, one non-word non-whitespace character, and a length of 8 or more.
The four (?=...) lookaheads enforce "must contain" without restricting the order of characters. The . at the end allows any character (including Unicode), so the password isn't artificially limited.
Add the lookahead (?=.*\d) at the start of the pattern. \d matches a digit; .* means "anywhere in the string"; the lookahead doesn't consume any characters so it composes cleanly with other rules.
To require exactly N digits, use (?=(?:\D*\d){N}\D*$): skip any number of non-digits, match a digit, repeat N times, then anchor to a non-digit-only tail.
An upper cap of around 64-128 characters is fine for protecting against denial-of-service from absurd inputs. Setting a much smaller maximum can exclude useful passphrases because long passwords (like passphrases) are much stronger than short complex ones when chosen unpredictably; length alone is not a strength guarantee.
NIST 800-63B specifically recommends accepting passwords up to at least 64 characters. Any "max 12" or "max 20" policy you see is either legacy or based on outdated advice.
No. The regex validates the input format only. Password storage is a separate problem: hash with bcrypt, scrypt, or argon2 (never MD5, SHA-256 alone, or any encryption). The hash includes a salt; you store the hash, not the password.
For application-specific guides, see the "stored safely" section below.
Because Go's regexp and Rust's regex crate do not include lookarounds in their supported syntax.
The workaround is to run separate regexes per requirement (one for each character class, plus a length check) and combine the booleans in code. Cleaner code anyway. Count Unicode characters consistently with the rest of your application.
Add a negative lookahead at the start: ^(?!.*password)(?!.*123456).{8,}$. Each (?!...) fails the match if the forbidden substring appears anywhere.
For "must not contain the username", you need a variable in the pattern, which is easier to do in code than regex: check password.toLowerCase().includes(username.toLowerCase()) separately and combine with the regex result.
No. Password1! passes a four-class regex but is predictable and may appear in breach datasets. For real security, run the candidate password through a breach check like the HaveIBeenPwned k-anonymity API before accepting it.
A format check and a blocklist check answer different questions. Neither guarantees that a password is unguessable.
Where the password gets stored
The regex enforces the password format at input time. The password also has to be stored safely. For application-specific guides:
- How to Change a WordPress Password covers the four reliable methods (admin dashboard, WP-CLI, direct database edit, lost-password email) and the bcrypt hash format WordPress 6.8+ uses.
- How to Reset a Forgotten MySQL Root Password covers
--skip-grant-tables,--init-file, and the auth-plugin changes that landed in MySQL 8.4.
See also
- Regex Lookaheads and Lookbehinds: the zero-width-assertion technique that makes "must contain a digit AND an uppercase letter" work in a single pattern
- How to Match Numbers with Regex: the cousin pattern for purely numeric inputs (PINs, OTPs)
- How to Match an Email Address with Regex: the other field on every signup form
- How to Validate a Credit Card Number with Regex: the checkout-form counterpart, and why regex alone is only the format pre-check before the Luhn checksum
- Regex Anchors: whole-string validation and the final-newline trap
- Regex Word Boundaries: occasionally useful for "must contain a complete word"-style password rules
- Regex Capturing Groups and Backreferences: the
(.)\1{2}trick for rejecting repeated characters - Regex Cheat Sheet: the wider syntax and engine compatibility reference
External reference: NIST Special Publication 800-63B-4 is the current authoritative guidance on password policies. It recommends length over complexity, breach checks, and against forced periodic resets.
Sources
Authoritative references this article was fact-checked against.
- NISTpages.nist.gov





