TechEarl

How to Validate Password Strength with Regex

Validate password strength with regex. Length checks, character-class requirements, lookahead patterns for mixed-case/digit/special enforcement, examples in JavaScript, Python, and PHP, engine notes, and common mistakes.

Ishan KarunaratneIshan Karunaratne⏱️ 10 min readUpdated
Validate password strength with regex. Length checks, character-class requirements, lookahead patterns for mixed-case/digit/special enforcement, examples in JavaScript, Python, and PHP, engine notes, common mistakes.

A password strength regex is a regular expression that enforces password complexity rules (minimum length and required character classes) at input time, before the value reaches the database. The most common production pattern is ^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$, which requires at least one lowercase letter, one uppercase letter, one digit, one special character, and a minimum length of 8.

The pattern works because each rule is a (?=...) lookahead, a zero-width assertion that checks "does this exist anywhere?" without consuming characters. That lets you combine or relax rules without rewriting the whole expression. Below I walk the building blocks (length, character classes, lookahead-enforced requirements), runnable code in JavaScript, Python, and PHP, engine notes, common bugs, and the cases where regex is the wrong tool for the job.

Two things to remember before you ship a password regex: short, complex passwords are weaker than long, simple ones (Tr0ub4dor&3 is famously worse than correct horse battery staple), and your regex enforces format only. Storing the hash safely is a separate problem.

Quick reference

Minimum 8 characters, no other rules:

code
^.{8,}$

8+ characters with at least one lowercase, one uppercase, one digit:

code
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).{8,}$

The full "strong" pattern (lower, upper, digit, special, 8+):

code
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$

NIST 800-63B style (8+ characters, no other rules, server-side rejects breached passwords):

code
^.{8,}$

Length-only patterns

Start simple. Length is the most important factor in password strength.

code
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:

code
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:

code
^(?=.*[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:

code
^(?=.*[a-z])(?=.*[A-Z])[a-zA-Z]{8,}$

Two lookaheads, both must succeed.

Require lowercase, uppercase, AND digit:

code
^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)[a-zA-Z0-9]{8,}$

Add a fourth class (special characters):

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

Or accept any non-word character as "special":

code
^(?=.*[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. This is the version I prefer in practice.

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:

code
^(?=(?:.*[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:

code
^(?=(?:.*[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:

code
^(?=(?:.*[a-z]){3})(?=(?:.*[A-Z]){2}).{8,}$

Forbidden patterns

To reject specific weak patterns, add negative lookaheads.

Reject passwords containing the word "password":

code
^(?!.*password).{8,}$

Reject passwords that are entirely numeric:

code
^(?!^\d+$).{8,}$

Reject passwords where the same character repeats 3+ times in a row:

code
^(?!.*(.)\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:

javascript
const STRONG = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$/;

function isStrongPassword(input) {
  return STRONG.test(input);
}

isStrongPassword("Password1!");     // true
isStrongPassword("password1!");     // false (no uppercase)
isStrongPassword("PASSWORD1!");     // false (no lowercase)
isStrongPassword("Password!");      // false (no digit)
isStrongPassword("Password11");     // false (no special)
isStrongPassword("Pa1!");           // false (too short)

Python:

python
import re

STRONG = re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$")

def is_strong_password(value: str) -> bool:
    return bool(STRONG.match(value))

is_strong_password("Password1!")    # True
is_strong_password("short1A!")      # True (exactly 8 chars, all classes)
is_strong_password("alllowercase1!") # False (no uppercase)

PHP:

php
function isStrongPassword(string $value): bool {
    $pattern = '/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^\w\s]).{8,}$/';
    return (bool) preg_match($pattern, $value);
}

isStrongPassword("Password1!");      // true
isStrongPassword("nodigit!Aaaa");    // false (no digit)

Engine compatibility

Lookaheads are the key feature these patterns use. Every modern engine supports them, but the lookbehind variant (for "must NOT end with a digit" style rules) is not as widely supported.

EngineLookahead (?=...)Negative lookahead (?!...)Lookbehind (?<=...)Unicode-aware \d/\w
JavaScriptWorksWorksWorks (ES2018+)\d is ASCII; \p{L} Unicode (with u flag)
Python (re)WorksWorksWorks (fixed-width only)\w is Unicode by default, \d ASCII unless re.UNICODE
Python (regex pkg)WorksWorksWorks (variable-width)Full Unicode by default
PHP (PCRE)WorksWorksWorksASCII unless (*UCP) mode
JavaWorksWorksWorks (fixed-width)ASCII by default, Unicode with Pattern.UNICODE_CHARACTER_CLASS
.NETWorksWorksWorks (variable-width)Unicode-aware by default for \w, ASCII for \d unless ECMAScript option
Go (RE2)Not supportedNot supportedNot supportedASCII
Rust (regex crate)Not supportedNot supportedNot supportedASCII
RubyWorksWorksWorksUnicode by default
POSIX ERE (grep -E)Not supportedNot supportedNot supportedASCII

For Go and Rust, the lookahead-based "must contain" patterns do not compile. The workaround is to run separate regexes per requirement and combine the booleans:

go
hasLower := regexp.MustCompile(`[a-z]`).MatchString(s)
hasUpper := regexp.MustCompile(`[A-Z]`).MatchString(s)
hasDigit := regexp.MustCompile(`\d`).MatchString(s)
hasSpecial := regexp.MustCompile(`[^\w\s]`).MatchString(s)
isStrong := len(s) >= 8 && hasLower && hasUpper && hasDigit && hasSpecial

This is also the cleaner code style anyway: the regex stays simple, the rules are visible at the call site.

Common mistakes

The bugs I've shipped or seen in code review.

Forgetting the anchors. A pattern without ^ and $ (or \A/\z) accepts a 20-character password that contains a valid 8-character substring. Always anchor for validation.

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 silently rejects passwords starting with the uppercase letter. 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.

Validating the password but not the storage. A regex confirms the password is strong on input. 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's in every breach corpus. For real security, check the password against the HaveIBeenPwned k-anonymity API before accepting it.

Test cases

InputStrong pattern resultReason
Password1!MatchAll four classes present, length 10
P@ssword1Match@ is the special character
password1!No matchNo uppercase
PASSWORD1!No matchNo lowercase
Password!No matchNo digit
Password11No matchNo special character
Pa1!No matchToo short (length 4)
Correct Horse 1!MatchSpaces allowed by .
Über stark 1!MatchUnicode allowed by .; Ü counts as uppercase

FAQ

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:

See also

External reference: NIST Special Publication 800-63B is the current authoritative guidance on password policies. It recommends length over complexity, breach checks, and against forced periodic resets.

TagsRegexRegular ExpressionsPassword ValidationSecurityLookaheadJavaScriptPythonPHP
Share
Ishan Karunaratne

Ishan Karunaratne

Tech Architect · Software Engineer · AI/DevOps

Tech architect and software engineer with 20+ years building software, Linux systems, and DevOps infrastructure, and lately working AI into the stack. Currently Chief Technology Officer at a healthcare tech startup, which is where most of these field notes come from.

Keep reading

Related posts

Validate a US phone number with regex. Practical pattern, stricter NANP pattern, JavaScript / Python / PHP examples, what it lets through, common mistakes, and a test table.

How to Validate a US Phone Number with Regex

Validate a US phone number with regex. The practical pattern, a stricter NANP version, runnable examples in JavaScript, Python, and PHP, what it still lets through, common mistakes, and a test table.

Match a URL with regex. http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments. JavaScript / Python / PHP examples, engine notes, parser alternative, common mistakes, test table.

How to Match a URL with Regex

Match a URL with regex. Covers http/https schemes, protocol-relative URLs, ports, paths, query strings, fragments, runnable JavaScript / Python / PHP, engine notes, and the URL parser alternative.