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

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


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.