The possibility you describe exists; as others have noted, it is a "man in the middle attack".
In order to thwart it, you would need an additional secret that is not sent by email, such as (poor secret as it is) a cookie key. You can ask the user to reset the password using the same computer and browser used to send the "I want to reset my password" command.
Then, the attacker will be unable to send the appropriate cookie, because his domain being different from the original one, the browser won't have told him the appropriate cookie value to use.
A stricter possibility is to require the user to insert in the "Password reset" page, without closing it, a code that is being sent through email. The email at that point doesn't even contain a link. The attacker knows the code, but he hasn't control on the open browser window. The window may either contain a hidden secret that the server is "telling" to itself, or a cookie (which is basically the same thing). To avoid using persistent information, the server might store in the page a random string, and send the random string hashed with a secret salt. The user will then fill in a form that has both values (one hidden, one copied from the email). The server hashes the code with its secret salt. If the two strings are equal, the change is approved. To avoid "repeat attacks" in this case (i.e., reuse of a known challenge/hash pair), the check might include the user name and a timestamp:
form
username hidden (e.g. "lserni")
timestamp hidden (e.g. 20140108131005)
hash hidden (e.g. "e2961ca083b4393690ec74b93d3c4b32")
code input
The user receives the code "123456", at random from 100000 to 999999. There's one possibility in about nine hundred thousand of successfully guessing it. The server concatenates SERVERSECRETPASSWORD.lserni.20140108131005.123456 and verifies that it hashes to e2961ca083b4393690ec74b93d3c4b32
.
The attacker can guess the timestamp, but has no access to the hash. Knowing a hash and its corresponding code will only work on the rightful username, and only until the difference between the stored timestamp, that can't be changed, and the wall clock won't grow too large, at which point the server shall offer to send another email.
A possible (unavoidable) problem
On the other hand, if the attacker has control of the email, he can initiate a password recovery himself and be granted full access to the resource at least once. To guard against this, some other information (e.g. a "secret question") should have to be supplied to initiate password reset. Also, a warning in case of unsuccessful authentication should be issued to the user, so that he is made aware of the problem ("Wrong password. Please remember that you changed the password yesterday at 17:23 from IP 1.2.3.4. If you did not do that, please be advised that...");
Another possible issue
On some systems, you will be allowed to ask the server to "remember you". The server will do this by issuing a cookie that is, in every respect, a weak authentication and might no longer be connected with the password.
A change of password should invalidate all such cookies, otherwise the attacker can:
- take control of the email
- initiate a password reset
- intercept the password reset code
- delete the email with the password reset
- change the password
- ask the server "Remember me" and obtain a "Get Home Free" cookie
- profit
- the user will discover himself unable to login
- he will change the password and log back in
- the attacker will still be able to log in with his cookie.
One way to do this is to set the cookie to a random string plus the hash of a server secret, the random string, the user name, and the user's password hash in the database. A change of password will then automatically obsolete all extant authentication cookies for that user.