SVG Security Risks - not just a scalable graphic

Embedding Scalable Vector Graphics (SVG) can expose websites to code injection. This article explores how SVGs work, the risks they pose, and how to mitigate them.

For quite a while, I had the perception of Scalable Vector Graphics (SVG) being just that: an image that you could scale, due to the image elements being somehow semantically defined instead of pixel-by-pixel. While this intuition certainly isn't wrong, it is not complete either. The differences do have security implications, which this blog post aims to highlight. Being an introductory blog post, we'll stay on the surface and ignore more advanced attacks such as XML external entity injections or CSS exfiltration using SVG fonts.

What is an SVG?

The SVG standard is an open standard describing an image format in XML. Since it can define geometric shapes "semantically", an SVG can usually be rendered at or scaled to any resolution.

A minimal example of how an SVG may be defined is shown below:

<svg xmlns="http://www.w3.org/2000/svg">
    <circle cx="50" cy="50" r="30" fill="#DA3A00" />
</svg>
SVG of an orange circle
SVG of an orange circle

The xmlns attribute is needed to make browsers recognize the XML blob as an image to render instead of a simple XML file. circle defines a circle at coordinates (50, 50) on the canvas with a radius of 30, which is filled in a nice orange.

The SVG standard defines a multitude of other shapes and graphical knobs that you can combine and set any way you want. An SVG can also include other existing documents such as images and scripts. For example, you can make the circle blink, either through an animate tag

<svg xmlns="http://www.w3.org/2000/svg">
    <circle cx="50" cy="50" r="30" fill="#DA3A00">
    <animate
            attributeName="fill"
            values="#68768a;#DA3A00"
            calcMode="discrete"
            dur="0.2s"
            repeatCount="indefinite"/>
    </circle>
</svg>

or through a JavaScript snippet embedded in the SVG itself

<svg width="500" height="300" xmlns="http://www.w3.org/2000/svg">
    <circle id="circle" cx="50" cy="50" r="30" fill="#DA3A00" />
    <script>
        function setColor(color) {
            window.document.getElementById("circle").setAttribute("style", `fill: ${color}`)
        }

        function setOrange() {
            setColor("#DA3A00")
            setTimeout(setGrey, 200)
        }

        function setGrey() {
            setColor("#68768a")
            setTimeout(setOrange, 200)
        }

        setOrange()
    </script>
</svg>
Image of a blinking circle
Image of a blinking circle

Or we could combine our circle with another image, like so:

<svg xmlns="http://www.w3.org/2000/svg" width="300" height="250">
    <circle cx="150" cy="147.5" r="50" fill="#DA3A00" />
    <image href="S.svg" />
</svg>
Image of an S and an orange circle
Image of an S and an orange circle

(Since our CMS does not support images outside img tags, the above two images are not actual SVGs. You'll have to create these locally or head over to https://svg-demo.securesystems.dev/, where you can examine the original SVGs.)

Security risks of SVGs in HTML

That's a very basic overview of what the SVG format is capable of. The option to include scripts probably already rang your alarm bells for code injection. Essentially, SVGs can contain valid HTML which opens Pandora’s box of ‘XSS-type’ injection attacks. Now, what are the actual risks when including user-provided SVGs? Well, the available ‘features’ (or possible attacks) depend entirely on how the SVG is embedded in an HTML document.

For illustrative purposes, let's assume we create a website that displays a mind map, based on input of collaborating users. Each entry would be an SVG, sent to the server, which then outputs an HTML document to all users, incorporating the user SVGs at their respective canvas positions.

Several ways of embedding user-provided SVGs are possible:

  • img or image tags
  • iframe, embed, or object tags
  • inline

Besides different practical benefits and drawbacks listed in the MDN web docs, each of them has different security properties. Let's have a look!

Inline

The most naive way of adding SVGs to an HTML page is to include them inline, like so:

<!DOCTYPE html>
<html>
    <body>
        <div id="outside">I'm a div inside the HTML</div>
        <svg xmlns="http://www.w3.org/2000/svg" width="300" height="300">
            <circle cx="150" cy="147.5" r="50" fill="#DA3A00" />
            <image href="S.svg" />
            <script>alert("Hello from an SVG")</script>
            <script>document.getElementById("outside").innerHTML = "Got hacked"</script>
        </svg>
    </body>
</html>

This way, the SVG is completely unrestricted and can do pretty much anything. Which could be nice. And which most definitively is dangerous. In this case, the image loaded from inside your SVG displays correctly, so feature-wise the mind map is going strong. But then an alert pops up. And your div is vandalized. And an attacker can impersonate any user that opened a mind map with this SVG in it.

In fact, as they got script execution within the context of your webpage, an attacker can do many things. This comprises attacks like

  • extracting confidential information
    • reading everything on the page (e.g. personal information loaded in a header row of the mind map)
    • reading local storage
    • accessing credentials such as cookies missing the HttpOnly flag
  • impersonating the user using the origin's existing authentication (such as authentication cookies) for querying the API (e.g. changing the victim's account details or impersonating the victim and adding or editing post-its as them)
  • changing the SVG's parent HTML document through the window.parent handle accessible during same-origin embeddings (e.g. hiding all post-its, demanding payment, rendering a malicious page, or redirecting to one).

Additionally, embedding the user-generated SVGs inline requires the server to process them to some extent. While not in scope for this blog post, note that this increases the likelihood of XXE and SSRF attacks, which require server-side processing of the XML structure. So while inline SVGs definitively present a risk to your users, they may also present a risk to you as the server operator.

Embedding contexts

SVGs can also be included through iframe, embed, or object tags, each creating another browsing context.

<!DOCTYPE html>
<html>
    <body>
        <iframe src="circleWithS.svg" width="300" height="250"></iframe>
        <embed src="circleWithS.svg" width="300" height="250" />
        <object data="circleWithS.svg" width="300" height="250"></object>
    </body>
</html>

This loads the SVG in a more restricted context, which still allows the SVG to fetch external resources or execute scripts.

As shown above, script execution isn’t limited to benign uses such as making an element blink. Fortunately, it is limited to the context of the SVG, which is the origin from which the SVG was loaded. That is, due to the same-origin policy security feature of browsers, access from one HTML document to another is disallowed if their respective origins (the triple of scheme, host, and port) differ.

However, in case the SVG is loaded from the same origin as the parent HTML document (our mind map), this consequently allows an attacker to do evil things in the page's context just as if the SVG were included inline.

For example, if we have an HTML document

<!DOCTYPE html>
<html>
    <body>
        <div id="outside">I'm a div inside the HTML</div>
        <iframe src="attacker-controlled.svg"></iframe>
    </body>
</html>

and SVG attacker-controlled.svg containing

<svg xmlns="http://www.w3.org/2000/svg">
    <foreignObject>
        <script>window.parent.document.getElementById("outside").innerHTML = "Got hacked"</script>
    </foreignObject>
</svg>

then the div with id outside will be changed by the script as it was with the inline SVG above. You can see the example here.

Conversely, if the image is loaded from a different origin, we can get scripting functionality without compromising the main website. We could get blinking to work via JavaScript or we could load other images from within the SVG, which – spoiler – isn’t possible with the last integration method. This even works transitively, as SVGs don't need to load other images within an image tag, but can make use of a foreignObject tag and include an iframe, embed, or object therein to continue the chain:

<svg  version="1.1" xmlns="http://www.w3.org/2000/svg">
    <foreignObject>
        <iframe src="someOther.svg"></iframe>
    </foreignObject>
</svg>

Note though, that some scripts can still negatively impact your users. Imagine an embedded SVG, which alerts every few seconds. While the pop-up technically doesn’t originate from your embedding mind map, your users surely will not appreciate being annoyed constantly. Thus, even if embedded SVGs can’t directly affect your site, you still need to take some care.

Image tags

Lastly, img and image tags are strongly restricted by browser security controls. They do not load external content defined in SVG files and do not execute scripts. Thus, they're generally safe to use but can be insufficient if one of these features is required. For example, the circle made blinking through the use of JavaScript won't work and neither will the circle with an S (even if the S was hosted on the same origin). Thus the following HTML snippet would show the basic circle twice (the SVG will be rendered as much as possible), but neither blinking nor in combination with the externally loaded image.

<!DOCTYPE html>
<html>
    <body>
        <img src="circleBlinkJS.svg" />
        <img src="circleWithS.svg" />
    </body>
</html>

If our mind map application should support these features, or if the mind map implementation were a big SVG constructed from smaller ones, this doesn't work.

<svg xmlns="http://www.w3.org/2000/svg">
    <image href="userSvg1.svg" />
    <image href="userSvg2.svg" />
    ...
</svg>

So, as often, you need to strike a balance between security and usefulness.

Mitigation of SVG risks

Essentially, 4 mitigating measures exist for defending against the attacks showcased above:

  • You can employ img or image tags.
  • You can define a tailored Content Security Policy (CSP).
  • You can host the SVGs on a separate (sub)domain and embed them from there.
  • You can attempt input validation, sanitization, or output encoding when accepting or displaying SVGs.

If you intend to load an SVG like a regular image, without SVG-specific features, you'll usually want to load it inside an img or image tag. If that isn't possible, e.g. because you need external elements loaded by the SVG or you need scripting capabilities that SVGs offer, you should be a lot more careful. SVGs offer a powerful toolbox to both designers and attackers alike. If it’s required to display user-controlled or user-influenced SVGs on your site, a strong defensive measure is to define a CSP that restricts script execution for the SVGs, e.g. through script-src 'none' or hashes.

Another mitigation is to host the SVGs on a separate (sub)domain, e.g. resources.your-domain.org, used for static content only, because then the same-origin policy of your users' browsers will prevent any script execution within the context of your main site. In that case, even a CSP of script-src 'self' would protect you (in contrast to the case where you host both website and SVG on the same origin). This strong and simple defense-in-depth measure works only if you use an embedding context such as iframe because inlining obviously makes the resource load from your main site’s origin instead of the separate (sub)domain.

The remaining alternative is to attempt input validation, sanitization, or output encoding of the SVG on the server side to not include scripts. However, that surfaces the risk of parsing the XML structure without fail and also without running into issues such as XXE attacks. It is just more error-prone and thus to be avoided.

As such, the only reasonable solution for protecting against inlined SVGs is to use a tailored CSP, which, depending on your site’s setup, may be hard to devise. The more practical and easy way is to go for a two-pronged approach of using a separate (sub)domain for static assets in conjunction with a CSP that does not include it in its script-src directive.

Conclusion

Embedding user-provided SVGs can expose your website to injection attacks. This seems obvious at this point, but – going back to my initial statement – the most central learning is to keep in mind that an SVG is not just a scalable image, but a scripted object, which attackers will consider a juicy target for injection attacks. Avoid user-provided SVGs, if you can. If not, implement them with strong mitigating measures such as img tag, Content Security Policy, or cross-origin separation. Stay vigilant and take care.

Teetje Stark
Teetje is a Security Expert at SSE. He is part of our Defensive Security Team. As such he helps create secure applications both in design and implementation phase. He specializes in and is passionate about cryptography.