<?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: Gyu</title>
    <description>The latest articles on DEV Community by Gyu (@gyugyu86).</description>
    <link>https://dev.to/gyugyu86</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%2F4042098%2F1b700e17-0783-43cf-ac23-74518a95da8b.jpg</url>
      <title>DEV Community: Gyu</title>
      <link>https://dev.to/gyugyu86</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/gyugyu86"/>
    <language>en</language>
    <item>
      <title>No backend, no database, no network calls. I still found 3 security holes.</title>
      <dc:creator>Gyu</dc:creator>
      <pubDate>Sun, 26 Jul 2026 10:14:03 +0000</pubDate>
      <link>https://dev.to/gyugyu86/no-backend-no-database-no-network-calls-i-still-found-3-security-holes-4mi5</link>
      <guid>https://dev.to/gyugyu86/no-backend-no-database-no-network-calls-i-still-found-3-security-holes-4mi5</guid>
      <description>&lt;p&gt;No server. No database. No login. No network calls at all. One &lt;code&gt;index.html&lt;/code&gt; file, no build step. You open it in a browser, drop in some images, export, done.&lt;/p&gt;

&lt;p&gt;So when I sat down to audit it before pushing an update, I figured this would be quick. There's barely anything to attack.&lt;/p&gt;

&lt;p&gt;I found three things. None of them were in the rendering code, the canvas code, or anywhere I expected. They were all in the same place: &lt;strong&gt;the function that opens a project file.&lt;/strong&gt;&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an English write-up of &lt;a href="https://zenn.dev/gyugyu86/articles/36bff017df4e40" rel="noopener noreferrer"&gt;an article I posted on Zenn&lt;/a&gt;, restructured a bit for a different audience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The setup
&lt;/h2&gt;

&lt;p&gt;The tool makes App Store screenshots. The relevant feature: it saves your whole deck — text, images, styling — into a single &lt;code&gt;.json&lt;/code&gt; file, so you can pick up where you left off later, or &lt;strong&gt;hand it to someone else&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;That last part turned out to be the whole story.&lt;/p&gt;

&lt;h2&gt;
  
  
  1. A shared file could phone home
&lt;/h2&gt;

&lt;p&gt;Here's what the loader looked like:&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="c1"&gt;// before&lt;/span&gt;
&lt;span class="nx"&gt;DEVS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;sd&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;img&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;dev&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&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;im&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Image&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt; &lt;span class="nx"&gt;im&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;src&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;src&lt;/span&gt;&lt;span class="p"&gt;;&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;When &lt;em&gt;I&lt;/em&gt; save a file, every image is written out as &lt;code&gt;data:image/png;base64,...&lt;/code&gt;. So when reading one back, it'll be a data URL. Obviously.&lt;/p&gt;

&lt;p&gt;Except a &lt;code&gt;.json&lt;/code&gt; file is just text, and whoever has it can edit it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"style"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="nl"&gt;"bgImg"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://attacker.example/beacon.png?id=abc"&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Open that file and the browser goes and fetches it. The person on the other end gets your IP, your User-Agent, and a timestamp telling them you opened it. Nothing appears on screen. One image silently fails to load. There's nothing to notice.&lt;/p&gt;

&lt;p&gt;It's not RCE and it's not data exfiltration. But this tool's entire pitch is "runs locally, nothing is uploaded, no tracking" — and opening one file from someone else quietly breaks that. Less a severity problem, more a "the thing I promised isn't true" problem.&lt;/p&gt;

&lt;p&gt;The fix is a scheme allowlist:&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;IMG_DATA_RE&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/^data:image&lt;/span&gt;&lt;span class="se"&gt;\/(&lt;/span&gt;&lt;span class="sr"&gt;png|jpe&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;g|gif|webp|avif|svg&lt;/span&gt;&lt;span class="se"&gt;\+&lt;/span&gt;&lt;span class="sr"&gt;xml&lt;/span&gt;&lt;span class="se"&gt;)&lt;/span&gt;&lt;span class="sr"&gt;;base64,&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;A-Za-z0-9+&lt;/span&gt;&lt;span class="se"&gt;/&lt;/span&gt;&lt;span class="sr"&gt;=&lt;/span&gt;&lt;span class="se"&gt;\s]&lt;/span&gt;&lt;span class="sr"&gt;+$/i&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;safeImgSrc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&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;(&lt;/span&gt;&lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;string&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nx"&gt;IMG_DATA_RE&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;v&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;v&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;null&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;Inline &lt;code&gt;data:&lt;/code&gt; images pass. Everything else becomes &lt;code&gt;null&lt;/code&gt;. I ran every image path through it — background image, logo overlay, and the per-device screenshots.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Worth grepping your own code for:&lt;/strong&gt; &lt;code&gt;img.src&lt;/code&gt;, &lt;code&gt;fetch&lt;/code&gt;, &lt;code&gt;a.href&lt;/code&gt;, &lt;code&gt;link.href&lt;/code&gt;, CSS &lt;code&gt;url()&lt;/code&gt;, &lt;code&gt;iframe.src&lt;/code&gt;. Any file-derived string reaching those, and no scheme check? Same hole.&lt;/p&gt;

&lt;h2&gt;
  
  
  2. &lt;code&gt;Object.assign&lt;/code&gt; + &lt;code&gt;JSON.parse&lt;/code&gt; = prototype pollution
&lt;/h2&gt;

&lt;p&gt;Same function, one line up:&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="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assign&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Looks like a completely ordinary merge. There's a trap in this specific pairing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;JSON.parse('{"__proto__":{...}}')&lt;/code&gt; produces an object with &lt;code&gt;__proto__&lt;/code&gt; as an &lt;strong&gt;own property&lt;/strong&gt;. The setter is not invoked here.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Object.assign&lt;/code&gt; copies using &lt;strong&gt;&lt;code&gt;[[Set]]&lt;/code&gt;&lt;/strong&gt; — equivalent to writing &lt;code&gt;target.__proto__ = {...}&lt;/code&gt;. &lt;strong&gt;Now&lt;/strong&gt; the setter fires.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Result: the target's prototype gets swapped for whatever the attacker supplied. It's not &lt;code&gt;Object.prototype&lt;/code&gt;-wide pollution, so the blast radius is limited, but keys I never defined start showing up in &lt;code&gt;in&lt;/code&gt; and &lt;code&gt;for...in&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;Object.assign&lt;/code&gt; also doesn't check anything else. &lt;code&gt;titleSize: 999&lt;/code&gt; goes straight through. So does &lt;code&gt;bg1: "red;background:url(...)"&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;So I dropped the merge and rebuilt each known key by hand:&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="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sanitizeStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="k"&gt;typeof&lt;/span&gt; &lt;span class="nx"&gt;r&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;object&lt;/span&gt;&lt;span class="dl"&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;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;style&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bgType&lt;/span&gt;    &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safePick&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bgType&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gradient&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;solid&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;image&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bgType&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bg1&lt;/span&gt;       &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safeHex&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bg1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bg1&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titleSize&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safeNum&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titleSize&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.01&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mf"&gt;0.3&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;titleSize&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bgImg&lt;/span&gt;     &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;safeImgSrc&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;bgImg&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...known keys only&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;__proto__&lt;/code&gt; now just falls through as an unknown key. Numbers get clamped, colors get format-checked. Not "reject the dangerous keys" — &lt;strong&gt;only pick up the safe ones&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're hand-rolling a deep merge, or using &lt;code&gt;structuredClone&lt;/code&gt; on parsed input, same question applies. You can also move to &lt;code&gt;Object.create(null)&lt;/code&gt; or explicitly strip &lt;code&gt;__proto__&lt;/code&gt;, but narrowing to known keys is the version I can read six months from now.&lt;/p&gt;

&lt;h2&gt;
  
  
  3. I trusted the types
&lt;/h2&gt;

&lt;p&gt;I'd written the types. I just never checked the values at runtime.&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="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtitle&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;|| ""&lt;/code&gt; was supposed to mean "empty becomes an empty string." But if &lt;code&gt;title&lt;/code&gt; is &lt;code&gt;12345&lt;/code&gt;, the number sails right through. The render path calls &lt;code&gt;title.trim()&lt;/code&gt;, so you get &lt;code&gt;trim is not a function&lt;/code&gt; and the app dies on the spot. &lt;code&gt;{}&lt;/code&gt; does the same thing.&lt;/p&gt;

&lt;p&gt;TypeScript wouldn't have saved me here either — &lt;code&gt;JSON.parse&lt;/code&gt; returns &lt;code&gt;any&lt;/code&gt;, and types don't exist at runtime.&lt;/p&gt;

&lt;p&gt;Counts had the same gap. The editor caps you at 10 slides. Through a file, you could hand it 50,000.&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="c1"&gt;// after&lt;/span&gt;
&lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;slides&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;sd&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;   &lt;span class="c1"&gt;// same cap the editor enforces&lt;/span&gt;
  &lt;span class="p"&gt;...&lt;/span&gt;
  &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;loc&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;L&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="na"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;safeStr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;tt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subtitle&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;500&lt;/span&gt;&lt;span class="p"&gt;)&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;Obvious in hindsight: &lt;strong&gt;UI validation is a property of the UI&lt;/strong&gt;, not of your data.&lt;/p&gt;

&lt;h2&gt;
  
  
  The bonus: a plain bug
&lt;/h2&gt;

&lt;p&gt;Not a vulnerability, just broken:&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="c1"&gt;// before&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;iphone&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;ipad&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This tool started with two devices. It now has seven — Android phone, iPhone 6.7″, Android tablet, Mac, Play feature graphic. I'd already refactored the export pipeline to iterate a shared &lt;code&gt;DEVS&lt;/code&gt; list. This one line never got updated.&lt;/p&gt;

&lt;p&gt;So: work on the Mac layout, save, reload the file, and you're silently back on iPhone. No error, no warning.&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="c1"&gt;// after&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;DEVS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="nx"&gt;state&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;d&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;device&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;I only caught it because the audit made me list every field coming out of the file and write down what validated it. A security inventory turning up ordinary bugs is pretty common, in my experience.&lt;/p&gt;

&lt;h2&gt;
  
  
  What was already fine
&lt;/h2&gt;

&lt;p&gt;Auditing isn't only about finding holes — confirming the clean parts is half the value.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;XSS: none.&lt;/strong&gt; User text hits &lt;code&gt;innerHTML&lt;/code&gt; in exactly one spot (the slide list chips) and it was already escaped. Text-node position, so escaping &lt;code&gt;&amp;amp; &amp;lt; &amp;gt; "&lt;/code&gt; is sufficient. I made the helper non-string-safe and added &lt;code&gt;'&lt;/code&gt; while I was there.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;&lt;code&gt;eval&lt;/code&gt; / &lt;code&gt;new Function&lt;/code&gt; / string &lt;code&gt;setTimeout&lt;/code&gt;: zero.&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;External resources: zero.&lt;/strong&gt; No CDN, no analytics, no web fonts — the CJK fonts are bundled.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CSS injection: unreachable.&lt;/strong&gt; There are &lt;code&gt;style.background&lt;/code&gt; writes, but only internal constants flow into them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;ZIP paths: safe.&lt;/strong&gt; Entry names are &lt;code&gt;{locale}/{device}_{NN}.png&lt;/code&gt;, both from internal constants. No user string in the path, so no &lt;code&gt;../&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Making "no network" provable, not just promised
&lt;/h2&gt;

&lt;p&gt;On top of the fixes, I added a CSP meta tag:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;meta&lt;/span&gt; &lt;span class="na"&gt;http-equiv=&lt;/span&gt;&lt;span class="s"&gt;"Content-Security-Policy"&lt;/span&gt; &lt;span class="na"&gt;content=&lt;/span&gt;&lt;span class="s"&gt;"
  default-src 'none'; img-src 'self' data: blob:; font-src 'self';
  style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline';
  connect-src 'none'; base-uri 'none'; form-action 'none'"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The important bit is that &lt;code&gt;img-src&lt;/code&gt; has no &lt;code&gt;http(s)&lt;/code&gt;. If a future refactor loses the validation, the browser blocks the outbound fetch anyway. &lt;code&gt;connect-src 'none'&lt;/code&gt; closes fetch, XHR and WebSockets.&lt;/p&gt;

&lt;p&gt;The whole app is one inline script, so &lt;code&gt;'unsafe-inline'&lt;/code&gt; has to stay. I'm fine with that trade — what I care about protecting here is "nothing leaves the machine," not "injected script can't run," and there's no injection vector to begin with.&lt;/p&gt;

&lt;p&gt;The side effect is nicer than the fix: "we don't track you" is a claim you have to take my word for. A CSP is something anyone can verify with the Network tab open.&lt;/p&gt;

&lt;h2&gt;
  
  
  Re-running the same attacks
&lt;/h2&gt;

&lt;p&gt;I don't consider a fix done until the original attack is re-run. So I built one malicious project file with everything in it and threw it at the app:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Attack&lt;/th&gt;
&lt;th&gt;Before&lt;/th&gt;
&lt;th&gt;After&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;bgImg&lt;/code&gt; set to a remote URL&lt;/td&gt;
&lt;td&gt;fetched it&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;null&lt;/code&gt;, zero requests&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;__proto__&lt;/code&gt; injected&lt;/td&gt;
&lt;td&gt;prototype swapped&lt;/td&gt;
&lt;td&gt;dropped as unknown key&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;titleSize: 999&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;applied as-is&lt;/td&gt;
&lt;td&gt;clamped to 0.3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;tags in a font name&lt;/td&gt;
&lt;td&gt;accepted&lt;/td&gt;
&lt;td&gt;default kept&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;50 slides&lt;/td&gt;
&lt;td&gt;all 50 built&lt;/td&gt;
&lt;td&gt;capped at 10&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;numeric title&lt;/td&gt;
&lt;td&gt;threw on &lt;code&gt;.trim()&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;coerced to &lt;code&gt;""&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;save on Mac, reload&lt;/td&gt;
&lt;td&gt;reverted to iPhone&lt;/td&gt;
&lt;td&gt;stays on Mac&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Network tab: zero requests. Console: no errors.&lt;/p&gt;

&lt;p&gt;And the part that's easy to skip — I checked that a &lt;em&gt;legitimate&lt;/em&gt; file still works. All six screenshots, styling, device and Japanese text survived the round trip, and export still hits spec (1284×2778, PNG color type 2, no alpha). Hardening that breaks the feature isn't hardening.&lt;/p&gt;

&lt;h2&gt;
  
  
  The takeaway
&lt;/h2&gt;

&lt;p&gt;I had the threat model backwards. I kept thinking about the network, because that's where input usually comes from. But this app's trust boundary was &lt;strong&gt;the file&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;The moment your app can open something a user obtained elsewhere, that's external input. &lt;code&gt;.json&lt;/code&gt;, &lt;code&gt;.csv&lt;/code&gt;, config files, export/import, "restore from backup." The trap is the sentence &lt;em&gt;"my app wrote this file, so I know what's in it."&lt;/em&gt; You wrote it. Someone else is handing it back.&lt;/p&gt;

&lt;p&gt;If you want the three-line version:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Find every place a file-derived string becomes a URL. Allowlist the scheme.&lt;/li&gt;
&lt;li&gt;Don't &lt;code&gt;Object.assign&lt;/code&gt; parsed JSON onto real state. Rebuild known keys.&lt;/li&gt;
&lt;li&gt;Whatever the UI enforces — types, lengths, counts — enforce it again on the file path.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One more thing, since a lot of us are shipping AI-written code now: models write the save and the load together, and the loader ends up assuming the saver's output. It'll cheerfully trust its own format. That's not something the model flags — you have to.&lt;/p&gt;

&lt;p&gt;The tool is &lt;a href="https://github.com/gyugyu86/app-store-screenshot-studio" rel="noopener noreferrer"&gt;on GitHub&lt;/a&gt; (MIT) if you want to look at the actual diff.&lt;/p&gt;

&lt;p&gt;Has anyone found something interesting in their own import path? I'm curious whether the &lt;code&gt;Object.assign&lt;/code&gt; one is as common as I suspect.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>security</category>
      <category>webdev</category>
      <category>programming</category>
    </item>
    <item>
      <title>Electron's docs quietly dropped their recommended security tool. What now?</title>
      <dc:creator>Gyu</dc:creator>
      <pubDate>Wed, 22 Jul 2026 13:41:36 +0000</pubDate>
      <link>https://dev.to/gyugyu86/electrons-docs-quietly-dropped-their-recommended-security-tool-what-now-23fm</link>
      <guid>https://dev.to/gyugyu86/electrons-docs-quietly-dropped-their-recommended-security-tool-what-now-23fm</guid>
      <description>&lt;h1&gt;
  
  
  Electron's docs quietly dropped their recommended security tool. What now?
&lt;/h1&gt;

&lt;p&gt;If you've worked on an Electron app, you've probably seen the &lt;a href="https://www.electronjs.org/docs/latest/tutorial/security" rel="noopener noreferrer"&gt;security checklist&lt;/a&gt; in the official docs. Until recently, that page also pointed to a static analysis tool called &lt;strong&gt;Electronegativity&lt;/strong&gt; to help you check it automatically.&lt;/p&gt;

&lt;p&gt;That recommendation is gone.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;PR: &lt;a href="https://github.com/electron/electron/pull/48878" rel="noopener noreferrer"&gt;electron/electron#48878 "docs: remove electronegativity"&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Closes: &lt;a href="https://github.com/electron/website/issues/906" rel="noopener noreferrer"&gt;electron/website#906&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So right now, the docs just say "here are 20 items, check them by hand, and keep checking them on every release" — no tool recommendation at all.&lt;/p&gt;

&lt;p&gt;This post covers what got removed, why, what you can use instead today, and (if you don't want to add a tool) the five spots worth checking by hand, with code.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;This is an English write-up of &lt;a href="https://qiita.com/gyugyu86/items/6aeec085623a9ea69411" rel="noopener noreferrer"&gt;an article I posted on Qiita&lt;/a&gt;, expanded a bit for a different audience.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Why it got removed
&lt;/h2&gt;

&lt;p&gt;Electronegativity was released by Doyensec back in 2019 — an OSS tool that parses AST and DOM to catch Electron-specific misconfigurations and anti-patterns. For years it was the one tool the official docs pointed to.&lt;/p&gt;

&lt;p&gt;Problem: feature development stopped around 2022. The repo itself says it's no longer being actively maintained, and it doesn't reliably keep up with newer Electron versions.&lt;/p&gt;

&lt;p&gt;There was a paid successor, &lt;strong&gt;ElectroNG&lt;/strong&gt; ($688/year, launched 2022), but that's not for sale anymore either — the buy page now just says to email them if you're interested in acquiring the source and infrastructure.&lt;/p&gt;

&lt;p&gt;So the entire "dedicated Electron static analysis tool" slot is empty right now.&lt;/p&gt;

&lt;h2&gt;
  
  
  What can you use instead
&lt;/h2&gt;

&lt;p&gt;Here's what I found when I went looking for what to actually run in CI.&lt;/p&gt;

&lt;h3&gt;
  
  
  CodeQL
&lt;/h3&gt;

&lt;p&gt;General-purpose, but its standard query set includes some &lt;code&gt;frameworks/electron&lt;/code&gt; queries that cover a handful of Electron-specific sinks. If you're already running CodeQL, this is close to free. It's not built around the Electron checklist specifically though, so don't expect full coverage.&lt;/p&gt;

&lt;h3&gt;
  
  
  Semgrep
&lt;/h3&gt;

&lt;p&gt;You can write your own rules for individual checklist items. This works well in practice — writing a handful of narrow rules for the exact things that bit &lt;em&gt;your&lt;/em&gt; app is a solid, low-effort first move.&lt;/p&gt;

&lt;p&gt;One catch: dataflow analysis (taint tracking) in the free tier only works within a single function's scope. Cross-function/cross-file taint tracking is a paid Pro feature.&lt;/p&gt;

&lt;h3&gt;
  
  
  What general tools miss
&lt;/h3&gt;

&lt;p&gt;ESLint plugins, &lt;code&gt;npm audit&lt;/code&gt;, Snyk — these catch dependency CVEs and general JS lint issues just fine. What they don't catch, as a group, is the stuff specific to Electron's process model: misconfigured &lt;code&gt;webPreferences&lt;/code&gt;, unsafe things exposed from preload, IPC handlers that don't validate their input.&lt;/p&gt;

&lt;h2&gt;
  
  
  Five things worth checking by hand
&lt;/h2&gt;

&lt;p&gt;Whether or not you add a tool, these are the spots I'd actually look at in review.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. webPreferences
&lt;/h3&gt;

&lt;p&gt;The basics, and also where most apps get bitten.&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="c1"&gt;// dangerous&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;nodeIntegration&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="na"&gt;contextIsolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&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;This gives the renderer direct access to Node. One XSS and you're looking at filesystem access or command execution.&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="c1"&gt;// fixed&lt;/span&gt;
&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;nodeIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;contextIsolation&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="na"&gt;sandbox&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="na"&gt;preload&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;path&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;__dirname&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;preload.js&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;),&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;Worth noting: Electron's own docs say &lt;code&gt;nodeIntegration: false&lt;/code&gt; alone isn't enough — you need &lt;code&gt;contextIsolation: true&lt;/code&gt; alongside it to actually block Node primitives. Treat these two as a pair.&lt;/p&gt;

&lt;p&gt;Also, &lt;code&gt;contextIsolation&lt;/code&gt; has been the default (&lt;code&gt;true&lt;/code&gt;) since Electron 12, and &lt;code&gt;sandbox&lt;/code&gt; since Electron 20. If you're on a reasonably current version and haven't explicitly turned these off, you're probably fine here already. &lt;strong&gt;The thing worth flagging in review is someone deliberately turning them off.&lt;/strong&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Inconsistent settings across windows
&lt;/h3&gt;

&lt;p&gt;This is the one I think gets missed most often.&lt;/p&gt;

&lt;p&gt;Main window is configured safely, but a child window creation site somewhere else in the codebase isn't.&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="c1"&gt;// main window: safe&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;mainWindow&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;contextIsolation&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="na"&gt;nodeIntegration&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="c1"&gt;// child window: got sloppy&lt;/span&gt;
&lt;span class="nx"&gt;ipcMain&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;open-win&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;arg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;BrowserWindow&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;webPreferences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;nodeIntegration&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="na"&gt;contextIsolation&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;false&lt;/span&gt; &lt;span class="p"&gt;},&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;If you only check the main window, you'll miss this every time. Worth diffing all the &lt;code&gt;BrowserWindow&lt;/code&gt; creation sites in a project against each other, not just eyeballing the "main" one.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Command execution, especially with privilege escalation
&lt;/h3&gt;

&lt;p&gt;Highest severity item on this list, full stop.&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="c1"&gt;// dangerous: untrusted value interpolated into a shell command,&lt;/span&gt;
&lt;span class="c1"&gt;// combined with privilege escalation&lt;/span&gt;
&lt;span class="nx"&gt;sudo&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`networksetup -setdnsservers Wi-Fi &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;userInput&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If &lt;code&gt;userInput&lt;/code&gt; isn't validated, an attacker gets arbitrary command execution with admin privileges. Combined with something like &lt;code&gt;sudo-prompt&lt;/code&gt;, the blast radius is the whole machine.&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="c1"&gt;// fixed: args as an array, no shell involved&lt;/span&gt;
&lt;span class="nf"&gt;execFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;networksetup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;-setdnsservers&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Wi-Fi&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;validatedInput&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="nx"&gt;options&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;execFile&lt;/code&gt; with an array of arguments means the value never gets re-interpreted as part of a command — as long as you don't pass &lt;code&gt;shell: true&lt;/code&gt;, which routes it back through a shell anyway.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. CSP
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight conf"&gt;&lt;code&gt;&lt;span class="n"&gt;default&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt;; &lt;span class="n"&gt;script&lt;/span&gt;-&lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="s1"&gt;'self'&lt;/span&gt; &lt;span class="s1"&gt;'unsafe-inline'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;unsafe-inline&lt;/code&gt; or &lt;code&gt;unsafe-eval&lt;/code&gt; in your CSP is a hole in your XSS defenses. A wildcard &lt;code&gt;*&lt;/code&gt; in a source list is the same problem.&lt;/p&gt;

&lt;p&gt;If you're writing a checker for this yourself: don't naively string-match for &lt;code&gt;*&lt;/code&gt;, or you'll flag partial wildcards like &lt;code&gt;*.example.com&lt;/code&gt; as false positives. Tokenize the CSP properly — split on &lt;code&gt;;&lt;/code&gt; for directives, then on whitespace for sources.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. shell.openExternal
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// dangerous: no scheme validation&lt;/span&gt;
&lt;span class="nx"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openExternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlFromRenderer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Pass anything other than &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt; (say, a &lt;code&gt;file:&lt;/code&gt; URL) and you can end up opening something you didn't intend to.&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="c1"&gt;// fixed: validate scheme against an allowlist&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;URL&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlFromRenderer&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;http:&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;shell&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;openExternal&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;urlFromRenderer&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;Same idea applies to &lt;code&gt;setWindowOpenHandler&lt;/code&gt; and &lt;code&gt;will-navigate&lt;/code&gt; — worth checking whether you're actually controlling navigation there too.&lt;/p&gt;

&lt;h2&gt;
  
  
  If you want to automate this
&lt;/h2&gt;

&lt;p&gt;Everything above is "remember to check this on every PR," which, honestly, doesn't hold up well in practice. A 40-file PR lands on a Friday afternoon and that one &lt;code&gt;webPreferences&lt;/code&gt; line gets missed. That's a CI problem, not a human-vigilance problem.&lt;/p&gt;

&lt;p&gt;So automating even a narrow slice of this is worth it. As mentioned above, writing 3-4 Semgrep rules for the specific things that have actually bitten your app is a good, cheap first step.&lt;/p&gt;

&lt;p&gt;Full disclosure since this is relevant: I've been building an OSS tool in this space called &lt;code&gt;electron-audit&lt;/code&gt; — parses JS/TS with Babel, never runs your app, outputs SARIF. The thing I cared about most is not becoming the boy who cried wolf: every finding carries a &lt;code&gt;confidence&lt;/code&gt; (high vs. heuristic) separate from severity, and the CI exit code only fails on high-confidence critical/high findings — so the softer, heuristic dataflow-based findings get reported but don't block your build.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/gyugyu86/electron-audit" rel="noopener noreferrer"&gt;https://github.com/gyugyu86/electron-audit&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you know of other tools trying to fill this same gap, genuinely curious — would like to hear about them.&lt;/p&gt;

&lt;h2&gt;
  
  
  tl;dr
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Electron's docs removed the Electronegativity recommendation (&lt;a href="https://github.com/electron/electron/pull/48878" rel="noopener noreferrer"&gt;#48878&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Reason: Electronegativity is unmaintained, and its paid successor ElectroNG isn't for sale anymore either&lt;/li&gt;
&lt;li&gt;Right now there's no tool the docs point you to&lt;/li&gt;
&lt;li&gt;Alternatives: CodeQL (general-purpose, a few Electron-specific queries), Semgrep (free tier taint is single-function only), or newer community OSS&lt;/li&gt;
&lt;li&gt;If you're not adding a tool, at minimum check: webPreferences, cross-window setting consistency, command execution, CSP, and openExternal&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The checklist itself isn't hard. Keeping it enforced release after release is the actual problem.&lt;/p&gt;

</description>
      <category>electron</category>
      <category>security</category>
      <category>typescript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
