Wordle banner
Ruby on Rails Security
May 4, 2022 - Robert Cotey

Wordle: A Case Study for Password Strength

A new-ish game owned by the New York Times, called “Wordle” has recently become very popular. In this game, your goal is to guess a five-letter word, in 6 guesses or less. After each guess you will be told if each letter is in the word, and if that letter is in the correct spot.

Wordle Example

In this example, you can see I started with the word Quest, and learned that E and S were valid letters in the wrong place. I then tried Bears, and learned that the E and the S were still in the wrong place but that the A was correct. From there I was able to figure out Shame, and it was correct.

That’s how the game works. But what makes for an interesting post, is the way we can tackle this game, and in some ways, how it can highlight the weakness of passwords, in general. Passwords don’t, generally, have a “right letter, wrong place” mechanism, but the wordle length of 5 letters and the fact that it’s a word makes it a good analogy for a weak password. In addition as a bonus the Mastermind nature of figuring out a word makes for an interesting programming challenge.

Limitations of Wordle

First, let’s acknowledge that Wordle is not a secure application. It’s a game and a public one at that. In fact, the word list is in the source code, and the way it picks words is trivial. That doesn’t make it less interesting to us, but it’s worth noting. We’re going to act like we’re trying to gain access to a secure area and getting the correct wordle gives us that access. But I want to stress that Wordle is a game and not even a secure game.

I also want to point out we are teaching you how to, effectively, become a wordle cheater. While that’s not our goal, that will be the end result. We just want to use Wordle, as an analogy for weak passwords and highlight some high-level ways bad actors can use to get around bad passwords.

Because Wordle is not a password system, they will do some things that password systems do not do. We should also refrain from holding Wordle to the standard of a traditional password system. Again, we’re using Wordle because it’s interesting, and easy to understand, not because it is a comprehensive password generation system.

Measurement of Success for This Experiment

One thing we need to do is talk about success, what it is, and what it means. We are going to consider it a success if we guess a word in 6 tries or less. But we also want to measure time. Time doesn’t really matter for Wordle, but it does for passwords. The longer it takes to guess a password, the better the password is. No password is perfect, but ideally, your passwords should take long enough to guess that you no longer use the service, or have changed the password. For example, if it takes 100 years to guess a password, then does it matter? So what we are looking for is: A correct guess in the least amount of time.

Methods for The Wordle Experiment

Again, this is an interesting programming challenge in addition to a good analogy. There are several methods that we could use to solve the wordle.

Were going to look at five different experiment methods.

  1. Brute force attack
  2. Modified Brute force Attack
  3. Dictionary Attack
  4. Modified Dictionary Attack
  5. Previous Information Attack

Each of these methods is very common for first pass attempts at guessing passwords.

In addition to verifying our answers on the Wordle website, we will also build a simple “Wordle” page that works about the same, so that we don’t spam the New York Times with requests.

Entrision’s “Wordle” Page

No need to roll our own application for this. We are really not trying to step on The New York Times, and this won’t be open to end users outside of Entrision. We’re trying to have a sample bit of logic so we don’t need to keep sending data to their servers.

# /app/controllers/wordle_controller.rb

def index
    session[:old_word] = session[:word]
    session[:word] = next_word
    session[:past_guesses] = []
    @guesses = []
  end

  def guess
    redirect_to '/wordle' unless session[:word]
    @guesses = session[:past_guesses] || []
    @guesses << params[:guess]
    session[:past_guesses] = @guesses

    render action: 'index'
  end
end


private


def words
  # these are the words Wordle will use in array form
end

def random_word
  words.sample
end

def next_word
  if session[:old_word]
    words[words.index(session[:old_word]) + 1]
  else
    random_word
  end

What we’re doing here is just laying out our basic Wordle emulator. It has the features we care about, but it’s not as feature-rich as The New York Times’ version. It’s enough that we don’t have to use their server to do our testing. Keep in mind that their implementation is actually in Javascript, so even the “real Wordle” doesn’t talk to their servers much. But let’s continue.

# /app/helpers/wordle_helper.rb

def decorate_word(correct, guess)
  return "<span class='badge bg-danger'>#{guess}</span>".html_safe if guess.length < 5

  word = []
  chars = guess.chars
  chars.each_with_index do |letter, i|
    color = 'secondary'
    color = 'warning' if match_letter(correct, letter, i).negative?
    color = 'success' if match_letter(correct, letter, i).positive?
    word << "<span class='badge bg-#{color}'>#{letter}</span>"
  end
  word.join('-').html_safe
end

def match_letter(correct, letter, position)
  # -1 = no position wrong, but letter right
  # 0 = no match at all
  # 1 = correct letter in position
  return 0 unless correct.chars.include? letter
  return 1 if correct.chars[position] == letter

  -1
end

This helper just does some matching and what not to give use those colors on the text. Notice that we just throw back sizes that are not 5.

I am not going to include the views. They are pretty standard and you can look in the repo should you be interested.

The Experiment Solvers

Again, we’re going to solve five different ways. But were looking at solving this in much the same way a password scanner would work. While there are better ways to solve, these methods are what are the most interesting. Also, it’s important to note that real-world password scanners are much more complex. This just gives us a high-level view, to talk about.

Our five ways to solve will be:

  • Brute Force Solver
  • Modified Brute Force
  • Dictionary
  • Modified Dictionary
  • Previous Information

Brute Force Solver

Let’s go for the easy way first and just do a straight-up brute force solver.

Brute Force Result

def brute_force
  chars = letters
  gusses = []
  chars.each do |a|
    chars.each do |b|
      chars.each do |c|
        chars.each do |d|
          chars.each do |e|
            return gusses, "#{a}#{b}#{c}#{d}#{e}" if "#{a}#{b}#{c}#{d}#{e}".downcase == session[:word].downcase

            gusses << "#{a}#{b}#{c}#{d}#{e}"
          end
        end
      end
    end
  end

  return 'fail', gusses
end

Here you can see the really basic brute force attack. We simply loop through every combination of possible letters. If we have a match then great if not, increment one letter and try again. This way is often thought of as slow, and in computer terms it is. But it’s still an attack that is used today because it’s easy. Look that method. Anyone can write it. However, in my test, even this very quickly written and not very well optimized “attack” took only 3.5 seconds.

What makes brute force attacks viable is their low bar of entry and the fact that you don’t need them to succeed 100% of the time. If they succeed 0.001% of the time then the “cost” is “worth it” because no real still is required.

The counter

If you’re a provider (a person providing a service), then your best bet is to log failed attempts and lockout accounts after a small number of attempts. In our example it took 2,535,180 tries to get it right. You should be able to log and block after say 100 and pretty effectively halt brute force attacks.

If you’re a user (a person using a service), then you can’t really count on the provider to do the right thing. Instead, you can help make your password secure by choosing a longer password. The more letters in your password, the longer it will take. Will it take long enough, well that’s hard to say. But what you can do is choose a longer password.

Modified Brute Force

This is like brute force but we use the rules of the system against it. From a Wordle example, this is not the best way to solve, but it’s interesting. From a password side of things this is analogous to a system build on other systems, and knowing the rules of the underlying system. Again this is very common, low-skill attack. You have a secure system built on insecure libraries (or rules), then your entire system really isn’t that secure.

Modified Brute Force Result

def mod_brute_force
  chars = letters
  good_chars = []
  solve = [nil, nil, nil, nil, nil]
  gusses = []
  chars.in_groups_of(5).each do |word|
    gusses << word.join('')
    word.each_with_index do |letter, index|
      next if letter.nil?

      good_chars << letter unless match_letter(session[:word], letter, index).zero?
      solve[index] = letter if match_letter(session[:word], letter, index).positive?
    end
  end

  good_chars.each do |letter|
    solve.each_with_index do |s, position|
      next unless s.blank?

      solve[position] = letter if match_letter(session[:word], letter, position).positive?
      gusses << solve.join('')
    end
  end

  return gusses, solve.join('')
end

Here, again, we can see that the code, while a little more complex (and to be fair I didn’t optimize this as all) is still very simple and yet because we know of a weakness we can take advantage of that. Here we took 0.0001 seconds, and only 14 attempts. This was doable because we knew a weakness in the system (one put there to make it a game).

You may be tempted to say, “But those don’t happen in real life.” But they do, and frequently. Rather it’s an overzealous admin that has draconian password rules like “must be between 6 and 20 letters numbers, no number may repeat more then once, numbers must not be sequential, and must contain at least one special letter that does not repeat.” (Those are just parameters for the “game”) Or it’s a system that is based on a weak or compromised library (heart bleed), it is very real and more common than you probably think.

The Counter

If you’re a provider, you have to make sure that your libraries are not compromised, and that your policies make sense and don’t make matters worse. For example, if you make someone choose a 16 letter or larger password and change it every 10 days a lot of people will choose something “dogilikenamehere!22” and change it to “dogilikenamehere!23”

If you’re a user, then there is not a lot you can do. The rules are set by the provider, and all you can do is follow them. But this is one of the reasons you should not share passwords between providers. More on that later.

Dictionary

This one got a lot of attention for a while and then attention died off, but it’s still a valid attack in use today.

Dictionary Result

def dictionary
  gusses = []
  dict.each do |word|
    gusses << word

    return gusses, word if word == session[:word]ight: 300;
      letter-spacing: 0.5px;
      line-height: 2.6rem;
  end

  return gusses, 'fails'
end

I won’t put the dictionary here because it would take a lot of space. It’s also important to note that this is a very simple way to try a dictionary attack. Essentially it just tries one word after another till it gets it right. Once again, a very low-skill attack, that is viable because of its low skill.

The Counter

If you’re a provider, you can try to discourage dictionary-based passwords. Rules like “Passwords can not contain a real word” might help, or might make things worse. This trivial example is very simple, but most real dictionary-based attacks will take a word say “loveandhate” and try variations like “l0v34ndh4t3” before moving on. The more complex you make your rules though, the harder you make it for your users. Again logging and locking seem like the best way to prevent success with these kinds of attacks.

If you’re a user, choose passwords not based on real words. Instead, choose passwords based on common phrases that you use all the time. For example, my grandmother always says “a hundred years ago….” She might make a password like “a100yrsa!go” it’s not perfect, but it’s easy to remember.

Modified Dictionary

This is the one that is most likely to solve Wordle the same way we people do.

Modified Dictionary Result

def mod_dictionary
  words = dict
  gusses = []
  while words.length > 1 do
    first = words.sample
    gusses << first
    first.chars.each_with_index do |letter, position|
      result = match_letter(session[:word], letter, position)
      if result.positive?
        words.reject!{ |w| w[position] != letter}
      elsif result.negative?
        words.reject!{ |w| !w.include? letter }
      else
        words.reject!{ |w| w.include? letter }
      end
    end
  end
  return gusses, words.first
end

What this does is basically what we do. It takes a word, guesses with it then excludes words based on letter inclusion and position. Uses that data to make the next guess. You can see it’s very fast and takes very few attempts. This is like a combination of the modified brute force and dictionary attacks. We know some of the system’s rules,and we use them to our advantage.

Again you may be tempted to say that it can’t happen in real life, but it does all the time. In fact, while more complex, this is probably the most frequent kind of successful attack. If I know a target is using a weak library, or that the library was compromised in some way, then using that knowledge in conjunction with a dictionary-based attack can often yield the desired result.

The counter

If you’re a provider, it is critical that your libraries and dependencies stay up to date. That is your best protection here.

If you’re a user, your best option is to hold the provider accountable. There isn’t a lot more you can do.

Previous Information

This kind of attack relies on having some kind of previous information from inside. Maybe you got access to a user’s previous password. Maybe you know something internal to the company.

Previous Information Result

def previous_info
  test_word = words[words.index(session[:old_word]) + 1]
  return [test_word], test_word if test_word == session[:word]

  mod_dictionary
end

Yep, that’s it. We know (cause we wrote it in this case) what today’s word is based on yesterday’s word.

Once again you will want to say “But that can’t happen in the real world” but it does. Take our user a few examples above. If I know they increment their password and their last password was “dogilikenamehere!23” then I know their next password is “dogilikenamehere!24”. If I know that they do this, and I don’t know exactly when they last changed their password I might have to try “dogilikenamehere!23”, “dogilikenamehere!24”, “dogilikenamehere!25”, and “dogilikenamehere!26” before I can “get in”. But you don’t even write code for that. You just copy and paste.

The counter

If you’re a provider, you have to guard all the security data well. Not just current information.

If you’re a user, make sure to use unpredictable variations in your password. There’s not a lot more you can do.

The “Winner” of the Wordle Experiment

The best Wordle solver is mod_dictonary though, to be fair, that only works if we exclude previous_info. However, I hope this exercise has shown a little bit about passwords and security.

But what can I really do to strengthen my passwords?

So after reading all of that. If you’re really concerned about your password (and you should be) there are a few things you can do.

  1. Advocate for multi-factor or two-factor authentication. It’s annoying, but it works well to stop these low skill attacks.
  2. look into “password-less” solutions for logins. We have outgrown passwords. They don’t work anymore and we need to move on. We are getting there, but it’s slow.
  3. While we wait for the world to catch up use a password manager to keep passwords for each service unique. That way, if one password is compromised, then it’s still just one service and not all of them.
  4. kook at single sign on service like Google. They can be a great way to make sure that even smaller sites use good authentication because it’s the another service (Google in this example), not the small site that does the authentication.

Bonus: Stay educated on these issues. By far that is the best way to make sure that you’re staying safe and following the best advice.