Search code examples
ruby-on-railsregexrubydeviseregex-lookarounds

How to include equal sign in password complexity regex lookahead?


I've literally copy pasted the sample password complexity regex in https://github.com/heartcombo/devise/wiki/How-To:-Set-up-simple-password-complexity-requirements.

password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/

I modified it to include the equal sign:

password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-\=]).{8,70}$/

or

password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-=]).{8,70}$/

However, when checking via irb, I encounter the following:

irb(main):001:0> 'Abcd0123' =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-]).{8,70}$/
=> nil
irb(main):002:0> 'Abcd0123' =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-\=]).{8,70}$/
=> 0
irb(main):003:0> 'Abcd0123' =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=.*?[#?!@$%^&*-=]).{8,70}$/
=> 0
  1. Why does the additional equal sign character incorrectly match Abcd0123 when clearly it does not have a special character?
  2. Is there a way to include the equal sign character as a special symbol?

n.b.

Mac OS X 10.14.6

checked with the following ruby versions:
ruby 2.3.1p112 (2016-04-26 revision 54768) [x86_64-darwin18] - irb 0.9.6(09/06/30)
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin18] - irb 1.2.1 (2019-12-24)

Solution

  • Short answer: [*-=] means "characters in the range from * to =", which actually means any of the following:

    *+,./0123456789:;<=
    

    To fix this, either put the - symbol at the start of the group, or the end of the group, or with a back-slash:

    # Good:
    [#?!@$%^&*=-]
    [-#?!@$%^&*=]
    [#?!@$%^&*\-=]
    
    # Bad:
    [#?!@$%^&*-=]
    

    Better answer: In my opinion, this is not a good software design! Why should a password be restricted to including one of these limited, arbitrary "special characters"? Why can't I use £ as a "special character"? Or <? Or ? Or ±? Or ~? ........

    You could instead use a named group, to include all non-alphanumeric characters as "special" characters:

    password =~ /^(?=.*?[A-Z])(?=.*?[a-z])(?=.*?[0-9])(?=[[:^alnum:]]).{8,70}$/