2

I am trying to have some password entries in AWS cloud formation templates. And to restrict the password to alpha numerical characters and subset of special characters. I tried the following:

"^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#@$?]).{8,}$"

This informs that there should be at least one lower and upper case along with one numerical digit. Also, restricts the special character set to [#@$?]

What I observed was that any special character if it were the last one was allowed. Anywhere else it was disallowed as expected

Thus, Hello*E1 is disallowed but HelloE1* was allowed.

How do I disallow all special characters except [#@$?] no matter where they appear?

Also, how to disallow entire special character set? "^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9]).{8,}$ didn't help. This allowed all special characters.

Any help is appreciated.

6
  • Is it possible to specify more than one regular expression? That would make it easier by far. – Kusalananda Sep 12 '17 at 19:49
  • @Kusalananda There is only one "AllowedPattern" that AWS cloud formation presents. I think we need to specify all rules in one go. – Prabhu Sep 12 '17 at 19:59
  • I think part of the problem is that your regular expression ensures at least one of those character classes, but does not restrict to only those character classes; not least with the very last bit: .{8,} meaning 'any character, eight or more times'. I think if you want to only permit those characters, it may be as simple as /[A-Za-z0-9#@$?]{8,}/. – DopeGhoti Sep 12 '17 at 20:11
  • The dot in a regex means any character, and you put .{8,}$ at the end. Is this what you want ? This looks wrong to me. – Anonymous Sep 12 '17 at 20:12
  • So the conditions are: 1) Password must be at least 8 characters long; 2) There must be at least one lower case, one upper case, and one number; 3) The only special characters allowed are [#@$?] (including the brackets or only what is whithin them?); 4) Order does not matter, as long as the 3 previous conditions are met. Am I right or I missed something? – nxnev Sep 12 '17 at 21:29
3

Regex:

^(?=[a-zA-Z0-9#@$?]{8,}$)(?=.*?[a-z])(?=.*?[A-Z])(?=.*?[0-9]).*

Explanation:

In the regex you provided, the lookaheads serves to ensure your string met some specific conditions, but they are not the real filter which keeps out undesired strings. You specified the following conditions:

  • (?=.*?[A-Z]): Match at least one uppercase letter.
  • (?=.*?[a-z]): Match at least one lowercase letter.
  • (?=.*?[0-9]): Match at least one number.
  • (?=.*?[#@$?]): Match at least one of these characters: #@$?
  • .{8,}: Match any character at least 8 times.

But there are at least two flaws:

  • The (?=.*?[#@$?]) part is unnecessary because those special characters are meant to be optional, not mandatory[1].

  • As I said before, you did not specify a real filter, so thanks to the .{8,} part, your regex will accept any string as long as it meets the conditions established by the lookaheads, even if it has undesired special characters[1].

So to solve those flaws it is necessary to:

  • Delete the (?=.*?[#@$?]) part.

  • Add a new lookahead that acts as the filter mentioned above.

To construct this filter, you should think "which characters I want to allow?" instead of "which characters I want to disallow?", because that is a easier scenario to handle in this specific case. If you say you only want a-z, A-Z, 0-9 and #@$? to be your allowed characters, then the lookahead should look like this:

(?=[a-zA-Z0-9#@$?])

But hey, in this step you can even set the minimum length and tell the lookahead where to start and where to stop (the start and end of string in this case):

(?=[a-zA-Z0-9#@$?]{8,}$)

I omitted the ^ here because it's already at the beggining of everything, so it's not necessary to be redundant. Now we just bring together all the lookaheads and match the valid password using .* instead of .{8,}:

^(?=[a-zA-Z0-9#@$?]{8,}$)(?=.*?[a-z])(?=.*?[A-Z])(?=.*?[0-9]).*

Note:

  1. Although, in the regex and examples you provided, I don't really know why: 1) #@$? were not treated as mandatory; 2) undesired characters were only allowed at the end of the string and not in another place. Maybe it has something to do with the regex engine used by AWS, because everything worked as expected when I tested it on my own.
1
  • Thanks @nxnev for the detailed answer. It was very useful. – Prabhu Sep 13 '17 at 12:19
3

We want a lookahead anchored at the string start for each required character type as well as the length requirement. Then a simple .* to slurp it all up:

^(?=[0-9a-zA-Z#@\$\?]{8,}$)(?=[^a-z]*[a-z])(?=[^A-Z]*[A-Z])(?=[^0-9]*[0-9]).*

Explanation:

First off, I decided to avoid use of the lazy quantifier when matching required char types (e.g. one uppercase) for the following reasons:

  • They are expensive.
  • Different regex engines have different ways of signifying lazy. (A couple don't support it at all.)
  • They aren't as common/familiar as the alternative.

So for efficiency, readability and "portability" I'm using the ^[^x]*[x] construct.

Now breaking the rest down...

^ : Everything anchored to the start

(?=[0-9a-zA-Z#@\$\?]{8,}$) : Lookahead with 8 or more of your allowed characters between start and end of string.

The next three use the same pattern: a lookahead matching zero or more of a char not matching a required char, then the required char. These are all anchored to the beginning so the effect is to allow a match of the required char at any position in the string:

(?=[^a-z]*[a-z]) : At least one lowercase.

(?=[^A-Z]*[A-Z]) : At least one uppercase.

(?=[^0-9]*[0-9]) : At least one digit.

.* : Everything above is lookahead which doesn't consume anything so consume it all here. The first lookahead makes sure the entire string is valid chars so this is safe.

I make no claims about this being optimized (except for avoiding lazy quantifier). This is simply one of the easier forms to comprehend.


Note: the cause of the problem you observed with #@$? is due to the lookahead not being anchored to the end of string. Any character will match after one of those four (and not necessarily just in the last position). Of course, you can't just add $ since that then crowds out valid characters. That's why I include all valid characters in the same lookahead.

9
  • @BLayer, thank you for the regex and the detailed explanation. – Prabhu Sep 13 '17 at 12:18
  • @StéphaneChazelas Did you read the article I sourced? Match the minimum (zero) and check for next token ([a-z]). No match? Backtrack and try again, this time at position one. The alternative doesn't do all that backtracking (of course, the real word difference is probably negligible but nonetheless). The dude behind that site eats, breathes and sleeps regex and apparently people pay him for consulting on such matters so I'm going to take his word for it. If someone proves him wrong I'm open to switching teams, though. – B Layer Sep 13 '17 at 12:29
  • @Prabhu. No problem. I accept upvotes as payment. :) – B Layer Sep 13 '17 at 12:30
  • One engine with one test string isn't going to prove much. I've read more than once that backtracking is considered a costly operation. Hard to imagine that matching the alphabet isn't highly optimized, too, at least in some engines. Anyways, if I get some time maybe I'll try a wider sample with a couple different engines. – B Layer Sep 13 '17 at 12:49
  • @StéphaneChazelas Go to regex101.com. Select pcre or python engine. Try various strings with my pattern and then with mine but using lazy quant. With the latter the number of steps required to match grows as the size of the input string grows while my pattern is constant at 20 steps. For some non-matches both grow but mine at half the rate of the other. For other non-matches lazy grows but mine is small+constant. My answer was meant to be engine agnostic (there was no mention of Perl prior to yours) and generically simple and efficient. I feel like I've achieved that goal. ;) – B Layer Sep 13 '17 at 13:35
2

Your regexp doesn't prohibit any character, it just requires one instance of every class. To restrict the allowed characters, change your . (which means any character) to the set of allowed characters:

^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#@$?])[a-zA-Z0-9#@$?]{8,}$

Note that if that's meant to be a perl string, you'd need to escape the $s and @s or use strong quotes ('...' or q{...}). You also want to make sure the regexp works in ASCII mode (a-z only matches English letters, and . matches any byte).

Remove the (?=.*?[#@$?]) if you don't require at least one of those symbols.

1
  • thanks for answering my question. And pointing out my wrong interpretation of my regex! – Prabhu Sep 13 '17 at 12:20

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.