javascripthtmlcanvasiframesandbox

Canvas drawing in a sandboxed iframe


I'm trying to draw into a canvas that is located in a sandboxed iframe. For some reason, this only works if allow-scripts is added to the sandbox white-list. I've tried finding any documentation on this limitation but couldn't. What's the reason? Is there a way to circumvent this? I cannot leave allow-scripts enabled because there the rest of the iframe content is based on untrusted input.

Canvas draw commands do seem pretty safe on the first glance but I must be missing some potential vector of attack so that browser vendors have decided to not allow doing so...

You can reproduce by accessing index.html on a server (e.g. python3 -m http.server). Opening index.html directly in the browser (as a file) won't work because of an invalid origin in that case.

Any pointers or even better, solutions, are greatly appreciated.

index.html

<html lang="en">

<head>
    <script>
        function updateCanvas() {
            const ctx = document.querySelector('iframe').contentDocument.querySelector('canvas').getContext('2d')
            ctx.fillRect(25, 25, 100, 100);
        }
    </script>
</head>

<body>
    <iframe sandbox="allow-same-origin allow-scripts" src="./iframe.html"></iframe>

    <button onclick="updateCanvas()">Draw</button>
</body>

</html>

iframe.html

<html lang="en">
<body><canvas></canvas></body>
</html>

Solution

  • This is because when a <canvas> is in an environment where scripting is disabled, the user agent must present its DOM content.

    [HTML] The canvas element

    In non-visual media, and in visual media if scripting is disabled for the canvas element or if support for canvas elements has been disabled, the canvas element represents its fallback content instead.

    Here you didn't set any content, so that was not obvious, but if you do set some content, you'll see it being rendered.

    <iframe sandbox="" srcdoc="<html lang=en>
      <body><canvas style='border: 1px solid green'>Canvas DOM content</canvas></body>
    </html>">

    Even <canvas> elements that are adopted from a context where scripting is enabled inside the frame will only render their DOM content.

    So instead of relying on the sandbox attribute, you may want to prevent scripting through Content-Security-Policy settings. This sounds a bit iffy to be honest, as it would make sense that an environment where script is blocked by CSP is going to be considered as scripting is disabled, but it currently isn't.
    So what you need to do is to either serve your iframe content with the Content-Security-Policy header set with script-src 'none', or to insert a <meta> tag in the <head> so it does the same. According to this issue, the <meta> might be a bit more future-proof, and if you go this way, I'd advise you to subscribe to that issue anyway so you can know when things will move.

    As a JSFiddle since StackSnippets don't allow cross-frame access.


    Note that if needed, another even more hackish workaround would be to replace the canvas content with a <video> fed by the <canvas> stream, but this just hits many browser bugs on their controls UI. Anyway, a fiddle for the adventurous ones.