I got it to work, but the solution is a bit complex, so bear with me.
What's happening
As it is, Internet Explorer gives lower level of trust to IFRAME pages (IE calls this "third-party" content). If the page inside the IFRAME doesn't have a Privacy Policy, its cookies are blocked (which is indicated by the eye icon in status bar, when you click on it, it shows you a list of blocked URLs).
(source: piskvor.org)
In this case, when cookies are blocked, session identifier is not sent, and the target script throws a 'session not found' error.
(I've tried setting the session identifier into the form and loading it from POST variables. This would have worked, but for political reasons I couldn't do that.)
It is possible to make the page inside the IFRAME more trusted: if the inner page sends a P3P header with a privacy policy that is acceptable to IE, the cookies will be accepted.
How to solve it
Create a p3p policy
A good starting point is the W3C tutorial. I've gone through it, downloaded the IBM Privacy Policy Editor and there I created a representation of the privacy policy and gave it a name to reference it by (here it was policy1
).
NOTE: at this point, you actually need to find out if your site has a privacy policy, and if not, create it - whether it collects user data, what kind of data, what it does with it, who has access to it, etc. You need to find this information and think about it. Just slapping together a few tags will not cut it. This step cannot be done purely in software, and may be highly political (e.g. "should we sell our click statistics?").
(e.g. "the site is operated by ACME Ltd., it uses anonymous per-session identifiers for its operation, collects user data only if explicitly permitted and only for the following purposes, the data is stored only as long as necessary, only our company has access to it, etc. etc.").
(When editing with this tool, it's possible to view errors/omissions in the policy. Also very useful is the tab "HTML Policy": at the bottom, it has a "Policy Evaluation" - a quick check if the policy will be blocked by IE's default settings)
The Editor exports to a .p3p file, which is an XML representation of the above policy. Also, it can export a "compact version" of this policy.
Link to the policy
Then a Policy Reference file (http://example.com/w3c/p3p.xml
) was needed (an index of privacy policies the site uses):
<META>
<POLICY-REFERENCES>
<POLICY-REF about="/w3c/example-com.p3p#policy1">
<INCLUDE>/</INCLUDE>
<COOKIE-INCLUDE/>
</POLICY-REF>
</POLICY-REFERENCES>
</META>
The <INCLUDE>
shows all URIs that will use this policy (in my case, the whole site). The policy file I've exported from the Editor was uploaded to http://example.com/w3c/example-com.p3p
Send the compact header with responses
I've set the webserver at example.com to send the compact header with responses, like this:
HTTP/1.1 200 OK
P3P: policyref="/w3c/p3p.xml", CP="IDC DSP COR IVAi IVDi OUR TST"
// ... other headers and content
policyref
is a relative URI to the Policy Reference file (which in turn references the privacy policies), CP
is the compact policy representation. Note that the combination of P3P headers in the example may not be applicable on your specific website; your P3P headers MUST truthfully represent your own privacy policy!
Profit!
In this configuration, the Evil Eye does not appear, the cookies are saved even in the IFRAME, and the application works.
Edit: What NOT to do, unless you like defending from lawsuits
Several people have suggested "just slap some tags into your P3P header, until the Evil Eye gives up".
The tags are not only a bunch of bits, they have real world meanings, and their use gives you real world responsibilities!
For example, pretending that you never collect user data might make the browser happy, but if you actually collect user data, the P3P is conflicting with reality. Plain and simple, you are purposefully lying to your users, and that might be criminal behavior in some countries. As in, "go to jail, do not collect $200".
A few examples (see p3pwriter for the full set of tags):
- NOI : "Web Site does not collected identified data." (as soon as there's any customization, a login, or any data collection (***** Analytics, anyone?), you must acknowledge it in your P3P)
- STP: Information is retained to meet the stated purpose. This requires information to be discarded at the earliest time possible. Sites MUST have a retention policy that establishes a destruction time table. The retention policy MUST be included in or linked from the site's human-readable privacy policy." (so if you send
STP
but don't have a retention policy, you may be committing fraud. How cool is that? Not at all.)
I'm not a lawyer, but I'm not willing to go to court to see if the P3P header is really legally binding or if you can promise your users anything without actually willing to honor your promises.
OK, let me put this bluntly: if you're putting user data, or anything derived from user data into a cookie for this purpose, you're doing something wrong.
There. I said it. Now we can move on to the actual answer.
What's wrong with hashing user data, you ask? Well, it comes down to exposure surface and security through obscurity.
Imagine for a second that you're an attacker. You see a cryptographic cookie set for the remember-me on your session. It's 32 characters wide. Gee. That may be an MD5...
Let's also imagine for a second that they know the algorithm that you used. For example:
md5(salt+username+ip+salt)
Now, all an attacker needs to do is brute force the "salt" (which isn't really a salt, but more on that later), and he can now generate all the fake tokens he wants with any username for his IP address! But brute-forcing a salt is hard, right? Absolutely. But modern day GPUs are exceedingly good at it. And unless you use sufficient randomness in it (make it large enough), it's going to fall quickly, and with it the keys to your castle.
In short, the only thing protecting you is the salt, which isn't really protecting you as much as you think.
But Wait!
All of that was predicated that the attacker knows the algorithm! If it's secret and confusing, then you're safe, right? WRONG. That line of thinking has a name: Security Through Obscurity, which should NEVER be relied upon.
The Better Way
The better way is to never let a user's information leave the server, except for the id.
When the user logs in, generate a large (128 to 256 bit) random token. Add that to a database table which maps the token to the userid, and then send it to the client in the cookie.
What if the attacker guesses the random token of another user?
Well, let's do some math here. We're generating a 128 bit random token. That means that there are:
possibilities = 2^128
possibilities = 3.4 * 10^38
Now, to show how absurdly large that number is, let's imagine every server on the internet (let's say 50,000,000 today) trying to brute-force that number at a rate of 1,000,000,000 per second each. In reality your servers would melt under such load, but let's play this out.
guesses_per_second = servers * guesses
guesses_per_second = 50,000,000 * 1,000,000,000
guesses_per_second = 50,000,000,000,000,000
So 50 quadrillion guesses per second. That's fast! Right?
time_to_guess = possibilities / guesses_per_second
time_to_guess = 3.4e38 / 50,000,000,000,000,000
time_to_guess = 6,800,000,000,000,000,000,000
So 6.8 sextillion seconds...
Let's try to bring that down to more friendly numbers.
215,626,585,489,599 years
Or even better:
47917 times the age of the universe
Yes, that's 47917 times the age of the universe...
Basically, it's not going to be cracked.
So to sum up:
The better approach that I recommend is to store the cookie with three parts.
function onLogin($user) {
$token = GenerateRandomToken(); // generate a token, should be 128 - 256 bit
storeTokenForUser($user, $token);
$cookie = $user . ':' . $token;
$mac = hash_hmac('sha256', $cookie, SECRET_KEY);
$cookie .= ':' . $mac;
setcookie('rememberme', $cookie);
}
Then, to validate:
function rememberMe() {
$cookie = isset($_COOKIE['rememberme']) ? $_COOKIE['rememberme'] : '';
if ($cookie) {
list ($user, $token, $mac) = explode(':', $cookie);
if (!hash_equals(hash_hmac('sha256', $user . ':' . $token, SECRET_KEY), $mac)) {
return false;
}
$usertoken = fetchTokenByUserName($user);
if (hash_equals($usertoken, $token)) {
logUserIn($user);
}
}
}
Note: Do not use the token or combination of user and token to lookup a record in your database. Always be sure to fetch a record based on the user and use a timing-safe comparison function to compare the fetched token afterwards. More about timing attacks.
Now, it's very important that the SECRET_KEY
be a cryptographic secret (generated by something like /dev/urandom
and/or derived from a high-entropy input). Also, GenerateRandomToken()
needs to be a strong random source (mt_rand()
is not nearly strong enough. Use a library, such as RandomLib or random_compat, or mcrypt_create_iv()
with DEV_URANDOM
)...
The hash_equals()
is to prevent timing attacks.
If you use a PHP version below PHP 5.6 the function hash_equals()
is not supported. In this case you can replace hash_equals()
with the timingSafeCompare function:
/**
* A timing safe equals comparison
*
* To prevent leaking length information, it is important
* that user input is always used as the second parameter.
*
* @param string $safe The internal (safe) value to be checked
* @param string $user The user submitted (unsafe) value
*
* @return boolean True if the two strings are identical.
*/
function timingSafeCompare($safe, $user) {
if (function_exists('hash_equals')) {
return hash_equals($safe, $user); // PHP 5.6
}
// Prevent issues if string length is 0
$safe .= chr(0);
$user .= chr(0);
// mbstring.func_overload can make strlen() return invalid numbers
// when operating on raw binary strings; force an 8bit charset here:
if (function_exists('mb_strlen')) {
$safeLen = mb_strlen($safe, '8bit');
$userLen = mb_strlen($user, '8bit');
} else {
$safeLen = strlen($safe);
$userLen = strlen($user);
}
// Set the result to the difference between the lengths
$result = $safeLen - $userLen;
// Note that we ALWAYS iterate over the user-supplied length
// This is to prevent leaking length information
for ($i = 0; $i < $userLen; $i++) {
// Using % here is a trick to prevent notices
// It's safe, since if the lengths are different
// $result is already non-0
$result |= (ord($safe[$i % $safeLen]) ^ ord($user[$i]));
}
// They are only identical strings if $result is exactly 0...
return $result === 0;
}
Best Answer
The concept is storing persistent data across page loads for a web visitor. Cookies store it directly on the client. Sessions use a cookie as a key of sorts, to associate with the data that is stored on the server side.
It is preferred to use sessions because the actual values are hidden from the client, and you control when the data expires and becomes invalid. If it was all based on cookies, a user (or hacker) could manipulate their cookie data and then play requests to your site.
Edit: I don't think there is any advantage to using cookies, other than simplicity. Look at it this way... Does the user have any reason to know their ID#? Typically I would say no, the user has no need for this information. Giving out information should be limited on a need to know basis. What if the user changes his cookie to have a different ID, how will your application respond? It's a security risk.
Before sessions were all the rage, I basically had my own implementation. I stored a unique cookie value on the client, and stored my persistent data in the database along with that cookie value. Then on page requests I matched up those values and had my persistent data without letting the client control what that was.