Wireless networks are an immense convenience, but they can be a security nightmare. Whether you’re sitting in a public wi-fi area or comfortably at home in front of your television, your connections to wireless devices are under constant threat. One of these threats involves vulnerabilities encountered when logging into an online service, when you can potentially reveal your account login and password information. Even if these values are encrypted, a smart hacker on the same wireless network might capture your traffic and attempt to replay it back to the server to gain access to your account.
For example, let’s say you’re sitting at a Starbucks and using their wi-fi to get your email. The email service will ask for your credentials in the form of your username and password. Obviously, if you send these values “in the clear,” you’re directly exposing yourself to potential hackers nearby. So instead of sending your password, you send a hashed version of your password, hashing it with a random value, R, that the server provides. In other words:
Server sends: R You reply: HASH(R + Hash(password)), R
True, you’re not revealing your password, because you’ve hashed it with the random value, and it would be very difficult – if not impossible – for a hacker to unwind the hash to reveal your password.
But he doesn’t have to!
He can simply copy your reply to the server and then send that exact same information back to the server (including copies of any cookies you sent), implementing a sort of “replay” attack. This could quite possibly give him access to your account.
Since I’m a big Ruby/Sinatra fan (as well as a Frank Sinatra fan), I was keen to find a way to prevent these types of “replay” attacks. One possible solution is to create a “Token Bank” within the server. A Token Bank keeps track of the latest tokens sent out to people logging into the system. This bank is ubiquitous, in that it is available to every Sinatra session on the server.
When a person requests access to an account, the Sinatra server generates a random value, R, and labels it a “Token.” It then places that token into the token bank and sends a copy to the client. The client will respond with the same token along with a hashed version of the token and the password. This incoming incoming token is checked against the values in the token bank. If the token is in the bank, it is immediately discarded, and the client login is allowed to proceed as normal. Meanwhile, if a hacker attempts a replay attack by logging in with the same credentials, the token is no longer in the bank, so the hacker’s login session gets terminated.
Here’s how the login page looks in Sinatra:
get '/login' do ... @login_token = SecureRandom.hex # require 'securerandom' token_bank.push(@login_token) token_bank.delete_at(0) if token_bank.length > MAX_LOGIN_TOKENS ... haml :login # Login page sends @login_token to client end
And here’s how you would handle the results coming back from the user:
post '/login' do ... redirect '/login' unless params.has_key?('login_token') && token_bank.include?(params['login_token']) token_bank.delete(params['login_token']) # Nuke before it's replayed! # Continue with normal login validation ... end
Because the token is immediately removed from the bank, any attempt at replaying the login session will fail.
Notice that there are a finite number of tokens that can be stored in the bank. You can keep this number pretty low, depending on the probability that you’ll have multiple logins happening simultaneously. For example, it might take a minute for a person to login to the system, and if you have, say 100 simultaneous people online during your peak hours, you can probably reduce the token bank size to 5 or 10. You can also monitor the size of the bank over time to ensure that it never gets exceeded. At the worst case, a user may have to attempt two logins to gain access to the system.