ChatGPT解决这个技术问题 Extra ChatGPT

Safari 3rd party cookie iframe trick no longer working?

So this is the umteenth revenge of the "how do I get 3rd party cookies to work in Safari" question but I'm asking again because I think the playing field has changed, perhaps after February 2012. One of the standard tricks to get 3rd party cookies in Safari was as follows: use some javascript to POST to a hidden iframe. It (used to) trick Safari into thinking that the user had interacted with the 3rd party content and so then allow cookies to be set.

I think this loophole has been closed in the wake of the mild scandal where it was revealed that Google was using that trick with its ads. At the very least, while using this trick I have been completely unable to set cookies in Safari. I unearthed some random internet postings that claimed that Apple was working on closing the loophole but I haven't found any official word.

As a fallback I even tried redesigning the main third party frame so that you had to click on a button before the content would load but even that level of direct interaction was not enough to melt Safari's cold cold heart.

So does anyone know for certain if Safari has indeed closed this loophole? If so, are there other workarounds (other than manually including a session ID in every request)?

Using a third party iframe that needs cookies is certainly not by definition a security attack! We run a webshop that is used in an iframe on a multitude of different domains, and have all kinds of problems with Safari as of late, so I'm also very interested in the answer to this (legitimate) question.
Pretty much everyone who builds Facebook apps has this problem with Safari. Facebook apps run in an iframe and by definition, all come from 3rd parties. This is why support for Safari on Facebook apps is a bit spotty: you can't use cookies.
I can't reproduce this issue on safari 5.1.7. It accepts the cookie from my facebook app iframe with the default "no third party cookies" setting. However Chrome 19.0.1084.46 with the same setting blocks the cookie.
Chrome 19+ with the (thankfully) non-default "Block third-party cookies and site data" option checked is /even harsher/ than Safari's default "Block cookies from third parties and advertisers" setting. In chrome, even if you visit the 3rd party domain and have cookies set, they will not be transmitted to the iframe. The user must actually add an "exception" for your domain in her Chrome security settings.
What do you mean with Feb 2012? Is there a technical change in Safari or a changing in law?

C
Community

Just wanted to leave a simple working solution here that does not require user interaction.

As I stated in a post I made:

Basically all you need to do is load your page on top.location, create the session and redirect it back to facebook.

Add this code in the top of your index.php and set $page_url to your application final tab/app URL and you’ll see your application will work without any problem.

<?php
    // START SAFARI SESSION FIX
    session_start();
    $page_url = "http://www.facebook.com/pages/.../...?sk=app_...";
    if (isset($_GET["start_session"]))
        die(header("Location:" . $page_url));

    if (!isset($_GET["sid"]))
        die(header("Location:?sid=" . session_id()));
    $sid = session_id();
    if (empty($sid) || $_GET["sid"] != $sid):
?>
   <script>
        top.window.location="?start_session=true";
    </script>
<?php
    endif;
    // END SAFARI SESSION FIX
?>

Note: This was made for facebook, but it would actually work within any other similar situations.

Edit 20-Dec-2012 - Maintaining Signed Request:

The above code does not maintain the requests post data, and you would loose the signed_request, if your application relies on signed request feel free to try the following code:

Note: This is still being tested properly and may be less stable than the first version. Use at your own risk / Feedback is appreciated.

(Thanks to CBroe for pointing me into the right direction here allowing to improve the solution)

// Start Session Fix
session_start();
$page_url = "http://www.facebook.com/pages/.../...?sk=app_...";
if (isset($_GET["start_session"]))
    die(header("Location:" . $page_url));
$sid = session_id();
if (!isset($_GET["sid"]))
{
    if(isset($_POST["signed_request"]))
       $_SESSION["signed_request"] = $_POST["signed_request"];
    die(header("Location:?sid=" . $sid));
}
if (empty($sid) || $_GET["sid"] != $sid)
    die('<script>top.window.location="?start_session=true";</script>');
// End Session Fix

Thank you, works like a charm and is so easy to implement in existing apps :-)
So this one basically works with a couple of redirects, right?
@hugoderhungrige you're welcome, I just added a new version, feel free to check it out if you need to maintain the signed request on your apps.
@CBroe Thank you very much for pointing that out! You were right, it works, since that by the second request the user already has a session started! I guess that "the worst blind is the one who doesn't want to see".
@Whiteagle Can you provide a test case demonstrating 1 and 2?
d
drewrichards

You said you were willing to have your users click a button before the content loads. My solution was to have a button open a new browser window. That window sets a cookie for my domain, refreshes the opener and then closes.

So your main script could look like:

<?php if(count($_COOKIE) > 0): ?>
<!--Main Content Stuff-->
<?php else: ?>
<a href="/safari_cookie_fix.php" target="_blank">Click here to load content</a>
<?php endif ?>

Then safari_cookie_fix.php looks like:

<?php
setcookie("safari_test", "1");
?>
<html>
    <head>
        <title>Safari Fix</title>
        <script type="text/javascript" src="/libraries/prototype.min.js"></script>
    </head>
    <body>
    <script type="text/javascript">
    document.observe('dom:loaded', function(){
        window.opener.location.reload();
        window.close();
    })
    </script>
    This window should close automatically
    </body>
</html>

I was thinking of something like that. Works flawlessly. I load it along with the permission dialog. Thanks!
Seems like this is also working around Safari's settings, and that, once it is common knowledge will get axed just like the other "solutions." I am looking for a different solution altogether, because it is becoming pretty obvious that 3rd party cookies are now the devil, even when used in appropriate ways.
This solution worked for me, however, is the popup necessary? Could you redirect your iframe to the safari page, set the cookie, then re-direct back to the game with redirect headers? Or do you need the popup so that the user has some form of direct contact with the server?
this worked well; fortunately, the pushing of a button to initialize this wasn't a big deal in my app.
@LocalPCGuy I'm not so sure it'll get axed like other solutions because it requires that a user actually click/interact with the page in order to have the popup not blocked. This solution that Safari has come up with seems to work well: advertisers won't be able to secretly set cross-domain cookies, and embedded apps will require a user to interact before being able to set a cookie.
v
vwoelm

I tricked Safari with a .htaccess:

#http://www.w3.org/P3P/validator.html
<IfModule mod_headers.c>
Header set P3P "policyref=\"/w3c/p3p.xml\", CP=\"NOI DSP COR NID CUR ADM DEV OUR BUS\""
Header set Set-Cookie "test_cookie=1"
</IfModule>

And it stopped working for me too. All my apps are losing the session in Safari and are redirecting out of Facebook. As I'm in a hurry to fix those apps, I'm currently searching for a solution. I'll keep you posted.

Edit (2012-04-06): Apparently Apple "fixed" it with 5.1.4. I'm sure this is the reaction to the Google-thing: "An issue existed in the enforcement of its cookie policy. Third-party websites could set cookies if the "Block Cookies" preference in Safari was set to the default setting of "From third parties and advertisers". http://support.apple.com/kb/HT5190


Apparently Apple "fixed" it with 5.1.4. I'm sure this is the reaction to the Google-thing: "An issue existed in the enforcement of its cookie policy. Third-party websites could set cookies if the "Block Cookies" preference in Safari was set to the default setting of "From third parties and advertisers". support.apple.com/kb/HT5190
So I think this comment by vwoelm is the closest to the answer that I was looking for. First and foremost, I wanted confirmation that Apple definitively closed the loophole and the reference to the Apple support article is just that. The second part of my question though still pertains. What are the range of options for workarounds. Clearly, we can encode session IDs as GET/POST params but what are the other options. Does local storage work in this context? Flash cookies?
@vwoelm: that is indeed the answer I was looking (but not hoping) for. If you put this in an answer instead of a comment, I'll assign you the bounty.
@gshurley I think sending session ids through GET/POST params is the only option. It's insecure, but then again Facebook forces us to serve canvas apps without SSL anyway. And besides what do you really get by hacking a Facebook app haha? We are at the mercy of Apple and they are currently in cruel mode.
F
Frank

For my specific situation I resolved the problem by using window.postMessage() and eliminating any user interaction. Note that this will only work if you can somehow execute js in the parent window. Either by having it include a js from your domain, or if you have direct access to the source.

In the iframe (domain-b) i check for the presence of a cookie and if it is not set will send a postMessage to the parent (domain-a). Eg;

if (navigator.userAgent.indexOf('Safari') != -1 && navigator.userAgent.indexOf('Chrome') == -1
    && document.cookie.indexOf("safari_cookie_fix") < 0) {
    window.parent.postMessage(JSON.stringify({ event: "safariCookieFix", data: {} }));
}

Then in the parent window (domain-a) listen for the event.

if (typeof window.addEventListener !== "undefined") {
    window.addEventListener("message", messageReceived, false);
}

function messageReceived (e) {
    var data;

    if (e.origin !== "http://www.domain-b.com") {
        return;
    }

    try {
        data = JSON.parse(e.data);
    }
    catch (err) {
        return;
    }

    if (typeof data !== "object" || typeof data.event !== "string" || typeof data.data === "undefined") {
        return;
    }

    if (data.event === "safariCookieFix") {
        window.location.href = e.origin + "/safari/cookiefix"; // Or whatever your url is
        return;
    }
}

Finally on your server (http://www.domain-b.com/safari/cookiefix) you set the cookie and redirect back to where the user came from. Below example is using ASP.NET MVC

public class SafariController : Controller
{
    [HttpGet]
    public ActionResult CookieFix()
    {
        Response.Cookies.Add(new HttpCookie("safari_cookie_fix", "1"));

        return Redirect(Request.UrlReferrer != null ? Request.UrlReferrer.OriginalString : "http://www.domain-a.com/");
    }

}

You are not getting a syntax error with your call to postMessage?
Need to add a second parameter "*" to PostMessage to fix the syntax error
Wish I could give this answer more upvotes. It's the simplest and most correct way to actually address this issue. Thank you for posting it!
j
joost

In your Ruby on Rails controller you can use:

private

before_filter :safari_cookie_fix

def safari_cookie_fix
  user_agent = UserAgent.parse(request.user_agent) # Uses useragent gem!
  if user_agent.browser == 'Safari' # we apply the fix..
    return if session[:safari_cookie_fixed] # it is already fixed.. continue
    if params[:safari_cookie_fix].present? # we should be top window and able to set cookies.. so fix the issue :)
      session[:safari_cookie_fixed] = true
      redirect_to params[:return_to]
    else
      # Redirect the top frame to your server..
      render :text => "<script>alert('start redirect');top.window.location='?safari_cookie_fix=true&return_to=#{set_your_return_url}';</script>"
    end
  end
end

Is there a similar problem with sessions in safari?
you can replace set_your_return_url to request.env['HTTP_REFERER'] as we are inside an iframe :-D
I have tried this solution, and the only issue is that I cannot return to the parent iframe url. Is there a method in Rails to obtain the url of the parent iframe? thanks
It took me time but finally I figured out that the easiest way (TMO) is to simply add it as a query-string parameter to the redirection url of the rails app. Wasy and clean.
This answer creates an open redirector, which is a common security problem. The dangerous code is redirect_to params[:return_to]. That param needs to be checked against a whitelist of safe places to redirect to. See owasp.org/index.php/…
S
Sascha Galley

I had the same problem and today I found a fix that works fine for me. If the user agent contains Safari and no cookies are set, I redirect the user to the OAuth Dialog:

<?php if ( ! count($_COOKIE) > 0 && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari')) { ?>
<script type="text/javascript">
    window.top.location.href = 'https://www.facebook.com/dialog/oauth/?client_id=APP_ID&redirect_uri=MY_TAB_URL&scope=SCOPE';
</script>
<?php } ?>

After authentication and asking for permissions the OAuth Dialog will redirect to my URI in the top location. So setting cookies is possible. For all of our canvas and page tab apps I have already included the following script:

<script type="text/javascript">
    if (top.location.href==location.href) top.location.href = 'MY_TAB_URL';
</script>

So the user will be redirected again to the Facebook page tab with a valid cookie already set and the signed request is posted again.


Great work on this. I updated the user agent check to make sure Chrome wasn't in the user agent as well since Chrome has Safari in the user agent string as well. Redirecting to your domain's page, setting the needed cookies/starting session and then redirecting back to your app on apps.facebook.com worked like a charm. Seems silly, but works well. Thanks Sascha for the tip!
M
Marcel Kalveram

I finally went for a similar solution to the one that Sascha provided, however with some little adjusting, since I'm setting the cookies explicitly in PHP:

// excecute this code if user has not authorized the application yet
// $facebook object must have been created before

$accessToken = $_COOKIE['access_token']

if ( empty($accessToken) && strpos($_SERVER['HTTP_USER_AGENT'], 'Safari') ) {

    $accessToken = $facebook->getAccessToken();
    $redirectUri = 'https://URL_WHERE_APP_IS_LOCATED?access_token=' . $accessToken;

} else {

    $redirectUri = 'https://apps.facebook.com/APP_NAMESPACE/';

}

// generate link to auth dialog
$linkToOauthDialog = $facebook->getLoginUrl(
    array(
        'scope'         =>  SCOPE_PARAMS,
        'redirect_uri'  =>  $redirectUri
    )
);

echo '<script>window.top.location.href="' . $linkToOauthDialog . '";</script>';

What this does is check if the cookie is available when the browser is safari. In the next step, we are on the application domain, namely the URI provided as URL_WHERE_APP_IS_LOCATED above.

if (isset($_GET['accessToken'])) {

    // cookie has a lifetime of only 10 seconds, so that after
    // authorization it will disappear
    setcookie("access_token", $_GET['accessToken'], 10); 

} else {

  // depending on your application specific requirements
  // redirect, call or execute authorization code again
  // with the cookie now set, this should return FB Graph results

}

So after being redirecting to the application domain, a cookie is set explicitly, and I redirect the user to the authorization process.

In my case (since I'm using CakePHP but it should work fine with any other MVC framework) I'm calling the login action again where the FB authorization is executed another time, and this time it succeeds due to the existing cookie.

After having authorized the app once, I didn't have any more problems using the app with Safari (5.1.6)

Hope that might help anyone.


this worked for me!! thanks!! .. i was having this problem with Safari 5.1.7.... now it's solved!
a
ar34z

I had this problem on devices running iOS. I made a shop that is embeddable in a normal website using an iframe. Somehow, on every pageload the user got a new sessionid, resulting in users getting stuck halfway the process because some values weren't present in the session.

I tried some of the solutions given on this page, but popups don't work very well on an iPad and I needed the most transparent solution.

I resolved it using a redirect. The website that embeds my site must first redirect the user to my site, so the top frame contains the url to my site, where I set a cookie and redirect the user to the proper page on the website that embeds my site, that is passed through in the url.

Example PHP code

Remote website redirects user to

http://clientname.example.com/init.php?redir=http://www.domain.com/shop/frame

init.php

<?php
// set a cookie for a year
setcookie('initialized','1',time() + 3600 * 24 * 365, '/', '.domain.com', false, false);
header('location: ' . $_GET['redir']);
die;

The user ends up on http://www.domain.com/shop/frame where my site is embedded, storing sessions as it should and eating cookies.

Hope this helps someone.


this actually worked very well, thanks! I didnt added any checks for browsers since there are a lot browsers actually and one redirect is not a problem
Y
Yara

Let me share my fix in ASP.NET MVC 4. The main idea like in correct answer for PHP. The next code added in main Layout in header near scripts section:

@if (Request.Browser.Browser=="Safari")
{
    string pageUrl = Request.Url.GetLeftPart(UriPartial.Path);
    if (Request.Params["safarifix"] != null && Request.Params["safarifix"] == "doSafariFix")
    {
        Session["IsActiveSession"] = true;
        Response.Redirect(pageUrl);
        Response.End();
    }
        else if(Session["IsActiveSession"]==null)
    {
        <script>top.window.location = "?safarifix=doSafariFix";</script>
    }
}

e
eye-wonder

This solution applies in some cases - if possible:

If the iframe content page uses a subdomain of the page containing the iframe, the cookie is no longer blocked.


This is useful information - just what I was looking for. While I'm sure most people can't change domains around, that's what I'm going to do. Have the site I'm embedding in create a ..com subdomain that maps to my IP and a VHost on my side for that domain and use that URL in the iFrame. Complicated, but should be bulletproof.
This can get complicated if you are using https with the subdomain, since the subdomain server will need an SSL certificate to match.
C
Colin Godsey

Google actually let the cat out of the bag on this one. They were using it for a while to access tracking cookies. It was fixed almost immediately by Apple =\

original Wall Street Journal post


I know about the Google thing, but I haven't seen anything about Apple actually “fixing” this (and breaking half the internet). Do you have any details on that?
T
Tom Kincaid

Here's some code that I use. I found that if I set any cookie from my site, then cookies magically work in the iframe from then on.

http://developsocialapps.com/foundations-of-a-facebook-app-framework/

 if (isset($_GET['setdefaultcookie'])) {
        // top level page, set default cookie then redirect back to canvas page
        setcookie ('default',"1",0,"/");
        $url = substr($_SERVER['REQUEST_URI'],strrpos($_SERVER['REQUEST_URI'],"/")+1);
        $url = str_replace("setdefaultcookie","defaultcookieset",$url);
        $url = $facebookapp->getCanvasUrl($url);
        echo "<html>\n<body>\n<script>\ntop.location.href='".$url."';\n</script></body></html>";
        exit();
    } else if ((!isset($_COOKIE['default'])) && (!isset($_GET['defaultcookieset']))) {
        // no default cookie, so we need to redirect to top level and set
        $url = $_SERVER['REQUEST_URI'];
        if (strpos($url,"?") === false) $url .= "?";
        else $url .= "&";
        $url .= "setdefaultcookie=1";
        echo "<html>\n<body>\n<script>\ntop.location.href='".$url."';\n</script></body></html>";
        exit();
    }

C
Charlie Schliesser

A slightly simper version in PHP of what others have posted:

if (!isset($_COOKIE, $_COOKIE['PHPSESSID'])) {
    print '<script>top.window.location="https://example.com/?start_session=true";</script>';
    exit();
}

if (isset($_GET['start_session'])) {
    header("Location: https://apps.facebook.com/YOUR_APP_ID/");
    exit();
}

u
user1351772

I have found the perfect answer to this, all thanks to a guy called Allan that deserves all of the credit here. (http://www.allannienhuis.com/archives/2013/11/03/blocked-3rd-party-session-cookies-in-iframes/)

His solution is simple and easy to understand.

On iframe content server (domain 2), add a file called startsession.php at the root domain level that contains:

<?php
// startsession.php
session_start();
$_SESSION['ensure_session'] = true;
die(header('location: '.$_GET['return']));

Now on the top level website containing the iframe (domain1), the call to the page containing the iframe should look like:

<a href="https://domain2/startsession.php?return=http://domain1/pageWithiFrame.html">page with iFrame</a>

And that's it! Simples :)

The reason this works is because you are directing the browser to a third party URL and thus telling it to trust it before showing content from it within the iframe.


c
cotko

I used modified (added signed_request param to the link) Whiteagle's trick and it worked ok for safari, but IE is constantly refreshing the page in that case. So my solution for safari and internet explorer is:

$fbapplink = 'https://apps.facebook.com/[appnamespace]/';
$isms = stripos($_SERVER['HTTP_USER_AGENT'], 'msie') !== false;

// safari fix
if(! $isms  && !isset($_SESSION['signed_request'])) {

    if (isset($_GET["start_session"])) {
        $_SESSION['signed_request'] = $_GET['signed_request'];
        die(header("Location:" . $fbapplink ));

    }
    if (!isset($_GET["sid"])) {
        die(header("Location:?sid=" . session_id() . '&signed_request='.$_REQUEST['signed_request']));
    }
    $sid = session_id();
    if (empty($sid) || $_GET["sid"] != $sid) {
    ?>
    <script>
        top.window.location="?start_session=true";
    </script>
    <?php
    exit;
    }
}

// IE fix
header('P3P: CP="CAO PSA OUR"');
header('P3P: CP="HONK"');


.. later in the code

$sr = $_REQUEST['signed_request'];
if($sr) {
        $_SESSION['signed_request'] = $sr;
} else {
        $sr = $_SESSION['signed_request'];
}

K
Karthik Keyan

I have also been suffering from this problem, but finally got the solution, Initially directly the load the iframe url in browser like small popup then only access the session values inside the iframe.


M
Mike Flynn

Safari now blocks all third party cookies. You can only use the Storage API to try to get user access to their third party cookies.

https://www.infoq.com/news/2020/04/safari-third-party-cookies-block/


A
AllTheCodez

Some context that I haven't seen clearly stated in the existing answers (and also a lot has changed since 2012!):

If you can control both the 3rd party iframe and the parent page (i.e. you are able to insert JavaScript on the parent page), then several workarounds are available. I would suggest the most elegant of these is making use of the postMessage API as described by @Frank's answer, as a) this does not require any redirects and b) does not require any user interactions.

If you do NOT control both the 3rd party iframe and the parent page, e.g. you have a widget hosted on a site you do not control, then most of the answers posted here will not work in Safari as of May 2020, and will stop working in Chrome around 2022. That is, unless a user has already visited your domain or interacts with the iframe, you are not able to set cookies. However, there are some commercial services offering solutions to solve this problem, such as CloudCookie.io


R
Rodrigue

I recently hit the same issue on Safari. The solution I figured out is based on the Local Storage HTML5 API. Using Local Storage you could emulate cookies.

Here's my blog post with details: http://log.scalemotion.com/2012/10/how-to-trick-safari-and-set-3rd-party.html


M
Manpreet Sethi

I decided to get rid of the $_SESSION variable all together & wrote a wrapper around memcache to mimic the session.

Check https://github.com/manpreetssethi/utils/blob/master/Session_manager.php

Use-case: The moment a user lands on the app, store the signed request using the Session_manager and since it's in the cache, you may access it on any page henceforth.

Note: This will not work when browsing privately in Safari since the session_id resets every time the page reloads. (Stupid Safari)


M
Madan

You can resolve this issue by adding header as p3p policy..i had same issue on safari so after adding header on top of the files has resolved my problem.

<?php
header('P3P:CP="IDC DSP COR ADM DEVi TAIi PSA PSD IVAi IVDi CONi HIS OUR IND CNT"');
?>

Are you sure this (still) works with the latest version of Safari? We've had P3P headers for years, for IE, but Safari's still broken.
Try clearing all of your cookies and test again. The problem doesn't show itself if your browser already has cookies for your iframed site.
Hmmm, I can't get the P3P headers to work either. They still work in IE though!
This used to work. But for the most recent version of Safari it does not. Clear your cache and try it again....
I want to state that this solution works. make sure you DELETE ALL cookies on safari. And then try this.