Search code examples
javascriptiframecross-domainsame-origin-policy

how can I access iframe.contentDocument to get response after cross-origin request?


I'm successfully sending a file from localhost:8888 to localhost:8080 (different domain in production), but I can't read the HTTP response after the transfer finishes.

Uncaught SecurityError: Failed to read the 'contentDocument' property from 'HTMLIFrameElement': Blocked a frame with origin "http://localhost:8888" from accessing a frame with origin "http://localhost:8080". The frame requesting access set "document.domain" to "localhost", but the frame being accessed did not. Both must set "document.domain" to the same value to allow access.

To send the file, for compatibility support, I'm trying to get this to work for <form> based file uploads; not XHR based. This is the basic HTML structure:

<form target="file-iframe" enctype="multipart/form-data" method="POST" action="invalid">
  <input type="file" id="file-input" class="file-input" title="select files">
</form>
<iframe src="javascript:false;" id="file-iframe" name="file-iframe"></iframe>

To insert the <iframe> element into the DOM, I do the following:

document.domain = document.domain;
var domainHack = 'javascript:document.write("<script type=text/javascript>document.domain=document.domain;</script>")';

var html = '<iframe id="file-iframe" name="file-iframe"></iframe>';
var parent = document.getElementById('wrapper');
var iframe = UTILS.createDomElement(html, parent);
iframe.src = domainHack;

UTILS.attachEvent(iframe, 'load', function(e) {

  // this throws the above SecurityError
  var doc = iframe.contentDocument || iframe.contentWindow.document;

  // do other stuff...
});

Before I submit the form, I set the action attribute on the <form> to be the target cross-domain URL:

action="http://localhost:8080/"

After submitting the <form>, the <iframe>'s load event is fired, and I try to access the <iframe>'s content to read the HTTP response. However, doing so throws the above error since this is is a cross-origin request, and I don't have access to the <iframe>'s content.

I thought the document.domain hack would work, but the error message is telling me that the iframe did not set the domain to localhost, even though I set the iframe's src attribute to the domainHack variable, which seems to execute.

Any ideas as to what I might be doing wrong? How can I set document.domain to be localhost for both the <iframe> and its parent (which is the current page).


I've read through several StackOverflow questions, some MDN articles, and other random results on Google, but I haven't been able to get this to work. Some stuff I've looked through already:


Solution

  • After digging around to try and figure this out, I finally came across a solution that seems to work for me. However, it isn't an exact answer to my question.

    In summary, I'm working on supporting <form> based file uploads. For browsers that do not support file upload via XHR, we have to resort to the traditional <form> submission, using a hidden <iframe> to avoid page refresh. The form redirects the reload action into the hidden <iframe>, and then the HTTP response is written to the body of the <iframe> after the file has been transferred.

    Due to the same-origin policy, and hence the reason I asked this question, I don't have access to the <iframe>'s content. The same-origin policy for <iframe>s restricts how a document or script loaded from one origin can interact with a resource from another origin. Since we can't get access to the <iframe>'s document, which is where the file upload HTTP response gets written to, the server will return a redirect response to which the server will append the upload response JSON. When the <iframe> loads, the JS will parse out the response JSON, and write it to the <iframe>'s body. Finally, since the redirect was to the same origin, we can access the <iframe>'s contents :)

    A huge thanks to jQuery File Uploader; they did all the hard work ;)

    https://github.com/blueimp/jQuery-File-Upload/wiki/Cross-domain-uploads

    Setting Up...

    JS

    function setupForm() {
    
      // form is declared outside of this scope
      form = document.createElement('form');
      form.setAttribute('id', 'upload-form');
      form.setAttribute('target', 'target-iframe');
      form.setAttribute('enctype', 'multipart/form-data');
      form.setAttribute('method', 'POST');
    
      // set the 'action' attribute before submitting the form
      form.setAttribute('action', 'invalid');
    };
    
    function setupIframe() {
    
      // iframe is declared outside of this scope
      iframe = document.createElement('iframe');
    
      /*
       * iframe needs to have the 'name' attribute set so that some versions of
       * IE and Firefox 3.6 don't open a new window/tab 
       */
      iframe.id = 'target-iframe';
      iframe.name = 'target-iframe';
    
      /*
       * "javascript:false" as initial iframe src to prevent warning popups on
       * HTTPS in IE6
       */
      iframe.src = 'javascript:false;';
      iframe.style.display = 'none';
    
      $(iframe).bind('load', function() {
        $(iframe)
          .unbind('load')
          .bind('load', function() {
            try {
    
              /*
               * the HTTP response will have been written to the body of the iframe.
               * we're assuming the server appended the response JSON to the URL,
               * and did the redirect correctly
               */
              var content = $(iframe).contents().find("body").html();
              response = $.parseJSON(content);
    
              if (!response) {
                // handle error
                return;
              }
    
              uploadFile(...); // upload the next file
            }
            catch (e) {
              // handle error
            }
          });
      });
    
      /*
       * insert the iframe as a sibling to the form. I don't think it really
       * matters where the iframe is on the page
       */
      $(form).after(iframe);
    };
    

    HTML - redirect page

    <!DOCTYPE html>
    <html lang="en">
      <head>
        <meta charset="utf-8">
      </head>
      <body>
        <script type="text/javascript">
          // grabs the JSON from the end of the URL
          document.body.innerHTML = decodeURIComponent(window.location.search.slice(1));
        </script>
      </body>
    </html>
    

    The only thing left to do is set the action attribute on the <form> to the cross-domain URL that we're sending the upload to:

    form.setAttribute('action', 'http://sub.example.com:8080/upload?id=ab123');
    form.submit();
    

    Meanwhile, on the server...

    // send redirect to the iframe redirect page, where the above HTML lives
    // generates URL: http://example.com/hack?%7B%22error%22%3Afalse%2C%22status%22%3A%22success%22%7D
    response.sendRedirect("http://example.com/hack?{\"error\":false,\"status\":\"success\"}");
    

    I know this is a massive hack to get around the same-origin policy with <iframe>s, but it seems to work, and I think it's pretty good with cross-browser compatibility. I haven't tested it in all browsers, but I'll get around to doing that, and post an update.