(Long post! You probably won’t think this is very interesting unless you play Muffins! and have a passing interest in cryptography and/or network security.)
When I started working on Muffins! over two years ago, I was a Japanese language student with no experience in programming or security whatsoever. I had heard about things like packet sniffing, though, and had a vague idea how the internets worked, but my ability to design a log-in system was limited by my ignorance of both PHP and of the possible vectors for attack.
Consequently, when Muffins! was just a blank page with a note saying “Imagine there’s a map here!”, the authentication mechanism sucked. Passwords were stored as unsalted MD5 hashes, and logging in sent your username and password in plaintext to the server, where the password was hashed and compared to the stored hash for your username. The server would then set a cookie with two fields: one for your user ID, and one for your password hash.
With every pageload, the server would look at your cookie and compare it to the contents of the database. If there was something wrong, it’d destroy your cookie and kick you to the log-in page, and that was that.
Obviously that arrangement sucked, and I was vaguely aware of it.1
My immediate “solution” was to also store the IP the player logged in from (which I wanted to do anyway to guard against multi abuse) and check that as well, so that if someone stole the player’s cookie and tried to use it from a different computer, it would fail. Of course, this was addressing the wrong problem; if someone got his hands on that cookie, it would be trivial to look up the hash in any one of the dozens of MD5 look-up databases out there, and then he could just log in normally.
A few months later, Boozerbear2 made an account and told me about Kingdom of Loathing‘s issues with cross-site request forgery attacks early on, and pointed out Muffins! had the same vulnerability.
hmmn, as far as i can see on cursory examination, you aren’t using a form submit key for any of your forms or action links. Jick calls his “pwd” in case you’re wondering what I mean, and it’s used to protect against XSS exploits against players.
It’s not serious now, but eventually you’ll have enough devious players who will take the time to craft trick url’s and (god forbid) inject an image tag somewhere with an onError event that forces people to send all thier muffins to another player or something equally nasty.
It’s a thing Jick had to deal with early on, and will probably rear its head here too, eventually. (:
My solution was to generate a form submit key at log-in, which basically consists of an MD5 hash of a salt, the password hash, and a random number. When a form is shown to the player, it now automatically has a hidden field (named “tok”) prefilled with this token, which is checked by the server when the player submits the form. Forms are used on pretty much every page in the game, so it took a while to implement, but it renders players pretty much immune to any CSRF worth anything3 (unless the attacker can somehow intercept the player’s HTTP traffic to obtain the token, of course, but at that point he has bigger things to worry about).
All well and good, until I had the thought that these tokens are far more secure than a plain MD5 of a password is (by then I had learned of MD5 look-up database on the internet), so why not use them for all authentication?
From then on, instead of an MD5 of the password, player cookies contained the form submit token. Can you spot why this was a bad idea?
Nonetheless, this was the situation until just now.
I just changed the log-in mechanism to generate a session key upon log-in as well, being an MD5 hash of the player ID, the password hash, and a random number. This is stored in the cookie now, along with the player ID, and in the database. It’s also only valid for up to twelve hours after the last pageload (so even careless users on public computers have some measure of protection), and is removed from the database when the user logs out using the log-out link.
Form submit keys still exist, but only in the forms themselves and in the database. I suppose it’s still possible to steal them, but it’s far less likely. Eventually they’ll be replaced by proper nonces4 and the point will be moot.
The IP is still checked. It’s not hard to get around (accidentally, even, with ISPs who still give their entire userbase a single IP), but it’s logged for other reasons anyway and trivial to check.
This basically reduces attack vectors to bruteforce (which is next on my security to-do list) and intercepting the plaintext password at log-in, which I can’t do much about without using TLS, which I don’t have the necessary permissions to set up on our server, I’m pretty sure. I am, however, looking into it.5
So, what’s the lesson here?
I’m a nub, but I learn. We all were, and we all do.
Also, I like to reinvent the wheel, apparently. Much of this is a result of my unwillingness to look at PHP’s inbuilt mechanisms for dealing with sessions. I imagine our current setup is quite similar to what they’ve had all along (maybe they use nonces, which would make it more secure, but which would also increase the overhead; given their general confusion about cryptography, I doubt it). At least this way I understand what I’m doing, and if it breaks I know where to start looking (and that it’s my fault).
Why am I so obsessed with securing our players’ passwords, you may ask? After all, if it’s compromised, all they’ve lost is an account on some game.
Well, yes, but most people don’t use different passwords for every single application out there (I know I don’t), even though they probably should, and the people who don’t and don’t consider the risks are also the ones most likely to complain at me. And I really, really don’t want to have to tell those kinds of people it was my fault. Being able to tell them it was entirely because of their failsauce password even a dictionary attack could guess is so much more satisfying.
And of course, I just enjoy doing this sort of thing. This side of Muffins! doesn’t require any drawings whatsoever.
1 It was worse than it sounded, actually, since the contents of the cookie weren’t escaped before being used in the MySQL query, so anyone with the ability to edit his cookie and a vague understanding of SQL could launch an SQL injection. Fortunately, no-one ever did, and I learned about
mysql_real_escape_string() relatively soon.
People often forget cookie data is user input as well, and has to be sanitised just the same as any POST text field.
3 If you don’t understand how, don’t worry about it. I may write a post about CSRF in the future, but it’s not worth explaining here.
4 I could use nonces for session keys, couldn’t I? Why don’t I? Effort, mostly, and overhead, and the fact that it completely breaks the user’s back button (moreso than it’s broken already because of all the POST data I can’t be bothered to deal with in an excessively userfriendly way).
This would still be vulnerable to replay attacks (unless the IP was also sent along, I suppose, though spoofing an IP isn’t that hard), but at least any eavesdropper wouldn’t be able to recover the plaintext password, which is important, given that users tend to use the same password for different things.