19
votes

Consider the following JavaScript snippet:

const app = document.getElementById('root');
const svg = `<svg version="1.1" id="Layer_1"...`;
const obj = document.createElement('object');

obj.setAttribute('type', 'image/svg+xml');
obj.setAttribute('data', `data:image/svg+xml; base64,${btoa(svg)}`);

app.appendChild(obj);

setTimeout(() => {
  console.log(obj.contentDocument.querySelector('svg'));
}, 1500);

(See this JSFiddle for a full example)

When this runs, the following error is given in the console (Google Chrome):

Uncaught DOMException: Failed to read the 'contentDocument' property from 'HTMLObjectElement': Blocked a frame with origin "https://fiddle.jshell.net" from accessing a cross-origin frame. at setTimeout (https://fiddle.jshell.net/_display:77:19)

With that in mind;

  1. Why is this considered a cross-origin request when trying to access the contentDocument of the object that has been created entirely dynamically, with no external resources?

  2. Is there a way to generate SVGs dynamically in this way, without offending the browsers cross-origin policy?

2
Fiddle is a 404. Does the same error happen if you don't base64 encode the svg?chiliNUT
I've updated the link, markdown was causing it to break: jsfiddle.net/4gqby5cygpmcadam
Works for me! Does ist work for you locally?Werner
@Werner No, this isn't working either locally or otherwise, in any browser. Can you elaborate on your comment. What is working for you exactly? Why do you think it's working for you? What browser and OS are you using? "Works for me", isn't really helpful.gpmcadam
I am running 62.0.3202.75 (32 bit) and can see the error in my console. Those who can't see it: maybe you have a filter applied to your console display that's hiding the error message? The error described in this question is the correct spec-compliant behavior for trying to access the contentDocument of an embedded context when that context has an opaque data: origin and the requesting context is a tuple origin. (html.spec.whatwg.org/multipage/…)apsillers

2 Answers

14
votes

The problem here is that data: URLs are treated as having a unique origin that differs from the origin of the context that created the embedded data: context:

Note: Data URLs are treated as unique opaque origins by modern browsers, rather than inheriting the origin of the settings object responsible for the navigation.

The WHATWG specification describes how content documents are accessed, which includes a cross origin check. The WHATWG same-origin comparison will never treat a traditional scheme-host-port "tuple" origin as equal to an "opaque" data: origin.

Instead, use Blob with URL.createObjectURL to generate a same-origin temporary URL whose contents will be readable by the outer environment:

var svgUrl = URL.createObjectURL(new Blob([svg], {'type':'image/svg+xml'}));
obj.setAttribute('data', svgUrl);

I don't know the security reason why this approach is allowed while a raw data: URL is not, but it does appear to work. (I guess because the generated URL is readable only by the origin that generated it, whereas a data: URL doesn't know how to be readable only by the original of its originating context.)

Note also that some versions of Internet Explorer support createObjectURL but erroneously treat the generated URLs as having a null origin, which would cause this approach to fail.

Other options are:

  1. Don't use a data: URL and instead serve the SVG content from the same origin as your page that creates the <object> element.

  2. Ditch the <object> and contentDocument altogether and use an inline <svg> element instead (fiddle):

    const obj = document.createElement('div');
    obj.innerHTML = svg;
    app.appendChild(obj);
    setTimeout(() => {
      console.log(obj.querySelector('svg'));
    }, 1500);
    

    Most browsers support inline <svg> elements (notably, IE 9.0+; other browsers much earlier). This means you can do

    <div>
        <svg>
            ...
        </svg>
    </div>
    

    and it will just render the SVG document inside the <div> as you would expect.

  3. Depending on what you want to do with the SVG, you can load it into a DOMParser and do DOM exploration/manipulation within the parser.

    var oParser = new DOMParser();
    var svgDOM = oParser.parseFromString(svg, "text/xml");
    console.log(svgDOM.documentElement.querySelector('path'));
    svgDOM.documentElement.querySelector('path').remove();
    

    But the DOM model will be separate from the SVG rendered in the <object>. To change the <object>, you need to serialize the parsed DOM structure and re-push it to the the data property:

    var oSerializer = new XMLSerializer();
    var sXML = oSerializer.serializeToString(svgDOM);
    obj.setAttribute('data', `data:image/svg+xml; base64,${btoa(sXML)}`);
    

    This doesn't seem super performant, because it needs the browser to re-parse a brand-new SVG document, but it will get around the security restrictions.

    Think of the <object> as a one-way black hole that can receive SVG information to render, but will not expose any information back. This isn't an informatic problem, though, since you have the information that you just fed into the <object>: there's nothing that contentDocument can tell you that you don't already know.

    However, if you want to make components within the SVG interactive by attaching listeners to components within the SVG structure that execute code on your main page, I don't think this approach will work. The separation between an <object> and its surrounding page has the same kind of embedding relationship as an <iframe>.

3
votes

because the object tag defines an embedded object within the HTML document, it's not part of the document itself, and therefore must respect the CORS like a frame

Same-origin policy

here clearly states that the content of the object tag is considered an external resource

The HTML element represents an external resource, which can be treated as an image, a nested browsing context, or a resource to be handled by a plugin.