Developer Toolbox

Regex: Strong password

Each (?=...) is a lookahead: it checks that something appears somewhere ahead without moving forward, so four of them in a row test four rules against the same string. Then .{12,} requires the length.

Open in Regex Tester The pattern and every example below are filled in.

Pattern

^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$

How it works

^
Start of the string.
(?=.*[a-z])
Somewhere ahead there is a lower-case letter.
(?=.*[A-Z])
An upper-case letter.
(?=.*\d)
A digit.
(?=.*[^A-Za-z0-9])
A character that is not a letter or a digit.
.{12,}
Twelve or more characters, which the lookaheads did not consume.
$
End of the string.

Matches

  • Tr0ub4dor&3xyz
  • correct-Horse-9-battery
  • Zz9!aaaaaaaa

Doesn't match

  • password123!
  • SHORT1!a
  • NoDigitsHere!!
  • Abcdefghijk12

In your language

JavaScript
/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$/

A literal; new RegExp(source, flags) builds the same from a string.

Python
re.compile(r"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$", re.ASCII)

A raw string, so backslashes reach re as written. re.ASCII keeps \d to 0-9, as in JavaScript (Python matches any Unicode digit otherwise). Use re.fullmatch to test a whole string.

Java
Pattern.compile("^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[^A-Za-z0-9]).{12,}$")

A normal string literal, so every backslash is doubled. matcher(s).matches() tests the whole string.

Go

RE2 has no lookahead. Check each rule with its own MatchString call, and the length with utf8.RuneCountInString(s) >= 12 (len counts bytes).

PHP
preg_match('/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$/D', $input)

PCRE with / delimiters inside a single-quoted string. Without the D modifier, $ also matches before a final newline, so it is added to patterns that end in $.

C#
new Regex(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[^A-Za-z0-9]).{12,}$", RegexOptions.ECMAScript)

A verbatim string: backslashes stay, a quote is doubled. RegexOptions.ECMAScript keeps \d to 0-9, as in JavaScript. $ also matches before a final newline; to reject one, end the pattern with \z instead.

Common mistakes

  • Length beats composition rules

    NIST SP 800-63B advises against rules like these, which push people towards Password1!. It recommends a minimum length, room for long passphrases, and checking new passwords against lists of breached ones.

  • A pattern cannot know a password leaked

    Password123! passes all four rules and appears in every breach list. Check candidates against such a list, for instance the Have I Been Pwned range API.

  • The dot skips line breaks

    Without the s flag, . does not match a newline, so a pasted password that ends in a line break fails the length check. Trim the input first.