Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does dynamically generating an SVG using HTMLObjectElement lead to a Cross-Origin error?

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?

like image 703
gpmcadam Avatar asked Oct 25 '17 10:10

gpmcadam


1 Answers

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>.

like image 105
apsillers Avatar answered Oct 19 '22 04:10

apsillers