Skip to content

SVGLoader: Loader does not work with strict content security policy #31206

@impact-merlinzerbe

Description

@impact-merlinzerbe

Description

When using a strict content-security-policy, svg files cannot be loaded properly if they contain inline styles. The errors are raised here

const xml = new DOMParser().parseFromString( text, 'image/svg+xml' ); // application/xml

I think that the DOMParser respects the csp and does not parse inline styles when they are not allowed by the csp. This breaks svgs e.g. when exported from inkscape because inkscape uses style attributes.

I think that inline styles/scripts/whatever are fine irrespective of the csp when loading an svg since threejs does not create dom nodes but maps the elements to threejs primitives, right?

If so, a possible solution might be to replace the DOMParser with another xml parser that is not influenced by the csp.

Reproduction steps

Use the following html file (e.g. with python -m http.server) and toggle the csp at the top:

<!doctype html>
<html>

<head>
  <!-- this works -->
  <!-- <meta http-equiv="Content-Security-Policy" -->
  <!--   content="default-src 'self'; script-src-elem 'nonce-randomnonce' https://cdn.jsdelivr.net; connect-src blob:; style-src 'unsafe-inline'" /> -->

  <!-- this does not work -->
  <meta http-equiv="Content-Security-Policy"
    content="default-src 'self'; script-src-elem 'nonce-randomnonce' https://cdn.jsdelivr.net; connect-src blob:" />

  <script type="importmap" nonce="randomnonce">
      {
        "imports": {
          "three": "https://cdn.jsdelivr.net/npm/[email protected]/build/three.module.js",
          "SVGLoader": "https://cdn.jsdelivr.net/npm/[email protected]/examples/jsm/loaders/SVGLoader.js"
        }
      }
    </script>

  <style nonce="randomnonce">
    body {
      margin: 0;
    }
  </style>
</head>

<body>
  <script type="module" nonce="randomnonce">
    import * as THREE from "three";
    import {SVGLoader} from "SVGLoader";

    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(
      70,
      window.innerWidth / window.innerHeight,
      0.1,
      1000,
    );
    camera.position.z = 2;

    const renderer = new THREE.WebGLRenderer({antialias: true});
    renderer.setSize(window.innerWidth, window.innerHeight);
    document.body.appendChild(renderer.domElement);

    const loader = new SVGLoader();

    const svgData = `
<svg
   viewBox="0 0 1 1"
   version="1.1"
   id="svg1"
   inkscape:version="1.4.1 (93de688d07, 2025-03-30)"
   sodipodi:docname="asset.svg"
   xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
   xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
   xmlns="http://www.w3.org/2000/svg"
   xmlns:svg="http://www.w3.org/2000/svg">
  <sodipodi:namedview
     id="namedview1"
     pagecolor="#ffffff"
     bordercolor="#000000"
     borderopacity="0.25"
     inkscape:showpageshadow="2"
     inkscape:pageopacity="0.0"
     inkscape:pagecheckerboard="0"
     inkscape:deskcolor="#d1d1d1"
     inkscape:document-units="mm"
     inkscape:zoom="256"
     inkscape:cx="2.0449219"
     inkscape:cy="1.0761719"
     inkscape:window-width="1916"
     inkscape:window-height="2020"
     inkscape:window-x="0"
     inkscape:window-y="138"
     inkscape:window-maximized="1"
     inkscape:current-layer="layer1" />
  <defs
     id="defs1" />
  <g
     inkscape:label="Layer 1"
     inkscape:groupmode="layer"
     id="layer1"
     transform="translate(-0.12072184,-0.08673415)">
    <rect
       style="fill:#888888;fill-opacity:0.933333;stroke:none;stroke-width:0.0173009;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
       id="rect1-2"
       width="0.84258819"
       height="0.88810742"
       x="0.19942777"
       y="0.14268047"
       rx="0.094999999"
       ry="0.094999999"
       inkscape:label="outline" />
    <rect
       style="fill:#f2f2f0;fill-opacity:0.933333;stroke:none;stroke-width:0.0168928;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
       id="rect1"
       width="0.82271105"
       height="0.86715645"
       x="0.20936632"
       y="0.15315592"
       rx="0.082271107"
       ry="0.086715646"
       inkscape:label="rect" />
    <path
       sodipodi:type="star"
       style="fill:#b7c4aa;fill-opacity:1;stroke:none;stroke-width:0.02;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
       id="north"
       inkscape:flatsided="true"
       sodipodi:sides="3"
       sodipodi:cx="0.58374268"
       sodipodi:cy="0.62210196"
       sodipodi:r1="0.13287176"
       sodipodi:r2="0.066435881"
       sodipodi:arg1="-1.5707963"
       sodipodi:arg2="-0.52359878"
       inkscape:rounded="0"
       inkscape:randomized="0"
       d="m 0.58374268,0.4892302 0.11507032,0.19930765 -0.23014064,-1e-8 z"
       inkscape:transform-center-y="-0.043083946"
       transform="matrix(0.47906062,0,0,1.2970083,0.34253532,-0.45760855)"
       inkscape:label="north" />
    <path
       sodipodi:type="star"
       style="fill:#b7c4aa;fill-opacity:1;stroke:none;stroke-width:0.02;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
       id="east"
       inkscape:flatsided="true"
       sodipodi:sides="3"
       sodipodi:cx="0.58374268"
       sodipodi:cy="0.62210196"
       sodipodi:r1="0.13287176"
       sodipodi:r2="0.066435881"
       sodipodi:arg1="-1.5707963"
       sodipodi:arg2="-0.52359878"
       inkscape:rounded="0"
       inkscape:randomized="0"
       d="m 0.58374268,0.4892302 0.11507032,0.19930765 -0.23014064,-1e-8 z"
       inkscape:transform-center-y="0.0010335321"
       transform="matrix(0,0.47906062,-1.2970083,0,1.6626323,0.30525927)"
       inkscape:transform-center-x="-0.27769491"
       inkscape:label="east" />
    <path
       sodipodi:type="star"
       style="fill:#b7c4aa;fill-opacity:1;stroke:none;stroke-width:0.02;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
       id="south"
       inkscape:flatsided="true"
       sodipodi:sides="3"
       sodipodi:cx="0.58374268"
       sodipodi:cy="0.62210196"
       sodipodi:r1="0.13287176"
       sodipodi:r2="0.066435881"
       sodipodi:arg1="-1.5707963"
       sodipodi:arg2="-0.52359878"
       inkscape:rounded="0"
       inkscape:randomized="0"
       d="m 0.58374268,0.4892302 0.11507032,0.19930765 -0.23014064,-1e-8 z"
       inkscape:transform-center-y="0.27769487"
       transform="matrix(-0.47906062,0,0,-1.2970083,0.89976452,1.6253562)"
       inkscape:transform-center-x="0.0010335321"
       inkscape:label="south" />
    <path
       sodipodi:type="star"
       style="fill:#b7c4aa;fill-opacity:1;stroke:none;stroke-width:0.02;stroke-linejoin:round;stroke-dasharray:none;stroke-opacity:1"
       id="west"
       inkscape:flatsided="true"
       sodipodi:sides="3"
       sodipodi:cx="0.58374268"
       sodipodi:cy="0.62210196"
       sodipodi:r1="0.13287176"
       sodipodi:r2="0.066435881"
       sodipodi:arg1="-1.5707963"
       sodipodi:arg2="-0.52359878"
       inkscape:rounded="0"
       inkscape:randomized="0"
       d="m 0.58374268,0.4892302 0.11507032,0.19930765 -0.23014064,-1e-8 z"
       inkscape:transform-center-y="-0.0010335321"
       transform="matrix(0,-0.47906062,1.2970083,0,-0.42033241,0.86248846)"
       inkscape:transform-center-x="0.27769487"
       inkscape:label="west" />
  </g>
</svg>
   `;

    const svgBlob = new Blob([svgData], {type: "image/svg+xml"});
    const url = URL.createObjectURL(svgBlob);

    loader.load(url, (data) => {
      const group = new THREE.Group();

      data.paths.forEach((path) => {
        const material = new THREE.MeshBasicMaterial({
          color: path.color,
          side: THREE.DoubleSide,
          depthWrite: false,
        });

        const shapes = SVGLoader.createShapes(path);

        shapes.forEach((shape) => {
          const geometry = new THREE.ShapeGeometry(shape);
          const mesh = new THREE.Mesh(geometry, material);
          group.add(mesh);
        });
      });

      group.scale.set(1, -1, 1); // Flip Y axis
      scene.add(group);
      animate();
    });

    function animate() {
      requestAnimationFrame(animate);
      renderer.render(scene, camera);
    }

    window.addEventListener("resize", () => {
      camera.aspect = window.innerWidth / window.innerHeight;
      camera.updateProjectionMatrix();
      renderer.setSize(window.innerWidth, window.innerHeight);
    });
  </script>
</body>

</html>

Code

// code goes here

Live example

Please use the reproduction html above.

Screenshots

No response

Version

r176.0

Device

Desktop

Browser

Chrome

OS

Linux

Metadata

Metadata

Assignees

No one assigned

    Labels

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions