Using a user-agent string to prevent session hijacking is roughly equivalent to a stupidity test. “Hello, I see you’re trying to hijack a session there. Why don’t you prove to me you can supply the target’s UA string?”
Session hijacking is particularly useful for hackers because anyone with a familiarity with protocol understands that login credentials can’t be sent in the clear. So devs and ops work SSL into their login pages and call it a day, providing a false sense of security even though the next page request after logging in can compromise the entire session. Oops.
It’s the equivalent of taking money out of an armored vault and then transporting it on the back of a pickup across the country. Your security protocol needs to be accountable for the entire lifetime of the transaction.
There are a few simple approaches to make session hijacking more difficult. The user-agent approach associates a session with the user-agent string that originally requested it, and invalidates the session if it changes. This is wrong. If someone has intercepted an HTTP request in transit, or worse, gained local access, then they already have the user-agent. It’s sent over the same TCP connection as the session!
In fact, you don’t even need local access or traffic sniffing. Just tricking the user into visiting a page controlled by the attacker is enough to get the user-agent. Some people seem to pick up on this and decide to modify the user-agent with a fancy crypto function to prevent this. They miss the point. Anything the client sends to the server can be intercepted and spoofed by the attacker.
There is no header you can send over HTTP to secure a connection. I have even heard people suggest that passing a hash of the user-agent as a GET variable will help validate the session. Protip: GET is sent in the HTTP request header.
This issue doesn’t seem to stop major web frameworks from using user-agent to “prevent” hijacking anyways. To add insult to injury, the technique might even cause false positives, because if your user is behind some sort of proxy cluster that modifies the user-agent, the value could change at any time, especially if the client is handed off to a different node.
A better solution is to associate a session with an IP address. However, this suffers from a few problems of its own. Firstly, users behind a NAT will all have the same external IP address, so it will be possible for anyone on the same network as the target to hijack the session. However, this greatly decreases the possible attackers for any given session, so it is an effective approach.
The primary benefit of IP over user-agent is that it’s much harder to spoof IP over a protocol like TCP (because it’s hard to read the replies), making the possibility of a successful attack based on IP spoofing virtually nil. The only real concern is that IP addresses for users can and do change frequently, especially for dialup users: sucks for them.
Another approach to add to your toolbox is session ID regeneration. Just issue a new ID for the session every couple of requests, making it hard for an attacker to get and keep a valid session cookie. This isn’t foolproof, and the window of opportunity for attackers depends on how many requests are made before you consider the ID expired. Only takes one POST to empty someone’s bank account.
Note that it is possible under the right combination of luck and circumstance that the attacker makes a request with a hijacked session and gets the new ID, logging the legitimate user out. If someone makes a request with an expired ID the only safe thing to do is to expire all subsequent IDs for that session as well, because you cannot differentiate user from attacker.
So what’s a developer to do? None of this is a problem if you transact the entire session over SSL. If security is a concern, there’s no excuse not to use full encryption.