<?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: jomynn</title>
    <description>The latest articles on DEV Community by jomynn (@jomynn).</description>
    <link>https://dev.to/jomynn</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%2F1281364%2F43541c24-85d1-4bb3-8236-e7ef7b1821b7.png</url>
      <title>DEV Community: jomynn</title>
      <link>https://dev.to/jomynn</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/jomynn"/>
    <language>en</language>
    <item>
      <title>XML External Entity (CWE-611): The Bug No Automated Layer Catches — Proven and Reported Entirely by Hand, Offline</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Thu, 03 Sep 2026 07:54:43 +0000</pubDate>
      <link>https://dev.to/jomynn/xml-external-entity-cwe-611-the-bug-no-automated-layer-catches-proven-and-reported-entirely-by-1bh</link>
      <guid>https://dev.to/jomynn/xml-external-entity-cwe-611-the-bug-no-automated-layer-catches-proven-and-reported-entirely-by-1bh</guid>
      <description>&lt;p&gt;One file path. No form, no captured traffic, no automated finding anywhere in the pipeline. Here's the&lt;br&gt;
full story of a real &lt;a href="https://cwe.mitre.org/data/definitions/611.html" rel="noopener noreferrer"&gt;CWE-611&lt;/a&gt; (XML External Entity)&lt;br&gt;
vulnerability — the first bug in this series where a static rule, a business-logic rule, &lt;em&gt;and&lt;/em&gt; a wired&lt;br&gt;
Active Test probe all come back empty, and every step from proof to report has to be done by hand — all&lt;br&gt;
running locally, with no cloud AI anywhere in the pipeline.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/lj3yU29Misg" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The vulnerable code
&lt;/h2&gt;

&lt;p&gt;The target is a deliberately vulnerable local training app: a hand-rolled XML "entity resolver" — not a&lt;br&gt;
real XML parser, on purpose, so the demo has no native dependencies — that reproduces the classic XXE&lt;br&gt;
pattern any real parser falls into when DTD/external-entity processing isn't explicitly disabled.&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;resolveEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&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;entities&lt;/span&gt; &lt;span class="o"&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;entityRe&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/&amp;lt;!ENTITY&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;(\w&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;+SYSTEM&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+"&lt;/span&gt;&lt;span class="se"&gt;([^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&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;*&amp;gt;/g&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;entityRe&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="nx"&gt;xml&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="p"&gt;[,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;systemId&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;m&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;filePath&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;systemId&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;replace&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/^file:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;// BUG: no restriction on which file&lt;/span&gt;
    &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFileSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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="k"&gt;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`[[failed to resolve entity &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&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;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="k"&gt;of&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;entries&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;entities&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`&amp;amp;&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;name&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="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;value&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="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/import&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;xml&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;resolveEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&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;noteMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;resolved&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&amp;lt;note&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;([\s\S]&lt;/span&gt;&lt;span class="sr"&gt;*&lt;/span&gt;&lt;span class="se"&gt;?)&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;note&amp;gt;/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;note&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;noteMatch&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;noteMatch&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&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="na"&gt;resolvedXml&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;resolved&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;Every declared &lt;code&gt;SYSTEM&lt;/code&gt; entity gets read off disk with no allowlist at all, and the whole resolved&lt;br&gt;
document — not just whatever lands inside &lt;code&gt;&amp;lt;note&amp;gt;&lt;/code&gt; — comes back in the response. &lt;code&gt;GET /&lt;/code&gt; renders&lt;br&gt;
instructions and the exact &lt;code&gt;curl&lt;/code&gt; command to try; there's no &lt;code&gt;&amp;lt;form&amp;gt;&lt;/code&gt; anywhere on this app, which turns&lt;br&gt;
out to matter for how you have to prove this one.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — Why the static rule engine stays silent (and that's not a bug)
&lt;/h2&gt;

&lt;p&gt;Before assuming "no finding" means "broken," it's worth tracing exactly why. The platform's XXE rule is&lt;br&gt;
real and precise — it just targets a different library's exact call shape than this app uses:&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="nf"&gt;parseXml&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;String&lt;/span&gt;&lt;span class="p"&gt;)?(&lt;/span&gt;&lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;noent&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="err"&gt;…&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;   &lt;span class="err"&gt;←&lt;/span&gt; &lt;span class="nx"&gt;the&lt;/span&gt; &lt;span class="nx"&gt;rule&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;s actual target: libxmljs&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="nx"&gt;opt&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="k"&gt;in&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This app never calls &lt;code&gt;parseXml&lt;/code&gt; or &lt;code&gt;parseXmlString&lt;/code&gt; at all — it's a regex-based resolver, deliberately, so&lt;br&gt;
the demo needs no native XML dependency. A code-scan pass produces &lt;strong&gt;zero CWE-611 findings&lt;/strong&gt;, exactly as&lt;br&gt;
the rule's own logic predicts. It isn't a clean zero, though: a generic missing-authorization rule flags&lt;br&gt;
both routes (this app, like the rest of the series, has no auth anywhere), landing a real &lt;strong&gt;High&lt;/strong&gt; finding&lt;br&gt;
on &lt;code&gt;POST /import&lt;/code&gt; — a second, entirely separate bug on the same route, worth its own line in a write-up&lt;br&gt;
rather than being lost in the noise.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — The dynamic/business-logic path comes back empty too
&lt;/h2&gt;

&lt;p&gt;The path that rescued CSRF and SSRF with at least one automated finding — capture traffic through the&lt;br&gt;
built-in proxy, then check it against the business-logic rules — doesn't help here either. Neither the&lt;br&gt;
live HTTP-interceptor rules nor the business-logic analyzer (both run inside the same &lt;code&gt;proxy&lt;/code&gt; command)&lt;br&gt;
have ever had an entity/DOCTYPE-aware check written for them. A captured request carrying a full XXE&lt;br&gt;
payload produces the same generic header findings as every other request and nothing else.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 3 — Proving it live, with nothing but curl
&lt;/h2&gt;

&lt;p&gt;There's no form to click through — &lt;code&gt;GET /&lt;/code&gt; is instructional only — so proof starts at the terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-X&lt;/span&gt; POST http://localhost:3008/import &lt;span class="nt"&gt;-H&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/xml"&lt;/span&gt; &lt;span class="nt"&gt;--data-binary&lt;/span&gt; @payload.xml
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&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="nl"&gt;"note"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"FLAG{xxe-demo-secret}"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="nl"&gt;"resolvedXml"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="s2"&gt;"...&amp;lt;note&amp;gt;FLAG{xxe-demo-secret}&amp;lt;/note&amp;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;That's the app's own intended file. The next payload proves it isn't confined to it — pointing the&lt;br&gt;
&lt;code&gt;SYSTEM&lt;/code&gt; identifier at &lt;code&gt;file:///etc/passwd&lt;/code&gt; instead returns that file's contents just as readily. The bug&lt;br&gt;
isn't "leaks one demo secret" — it's unrestricted file disclosure, any file the server process can read.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 4 — Escalating from scratch, because there's nothing to seed from
&lt;/h2&gt;

&lt;p&gt;Every prior bug in this series had a captured request to right-click and send into the built-in Repeater.&lt;br&gt;
Not this one — with no form and no proxy-captured traffic, the request has to be typed in by hand from a&lt;br&gt;
blank editor: method, URL, headers, body, all composed manually. A third payload variant makes a point&lt;br&gt;
worth keeping for a write-up:&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="cp"&gt;&amp;lt;?xml version="1.0"?&amp;gt;&lt;/span&gt;
&lt;span class="cp"&gt;&amp;lt;!DOCTYPE root [ &amp;lt;!ENTITY xxe SYSTEM "file:///etc/passwd"&amp;gt;&lt;/span&gt; &lt;span class="cp"&gt;&amp;lt;!ENTITY host SYSTEM "file:///etc/hostname"&amp;gt;&lt;/span&gt; ]&amp;gt;
&lt;span class="nt"&gt;&amp;lt;note&amp;gt;&lt;/span&gt;&lt;span class="ni"&gt;&amp;amp;xxe;&lt;/span&gt;&lt;span class="nt"&gt;&amp;lt;/note&amp;gt;&lt;/span&gt;
&lt;span class="c"&gt;&amp;lt;!-- &amp;amp;host; --&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The substitution is a literal string replace over the &lt;em&gt;whole&lt;/em&gt; submitted document, and the response's&lt;br&gt;
&lt;code&gt;resolvedXml&lt;/code&gt; field echoes the &lt;em&gt;entire&lt;/em&gt; resolved document — not just whatever ends up inside &lt;code&gt;&amp;lt;note&amp;gt;&lt;/code&gt;. The&lt;br&gt;
response shows &lt;code&gt;note&lt;/code&gt; still contains only &lt;code&gt;/etc/passwd&lt;/code&gt; (that field's own regex is scoped tightly), while&lt;br&gt;
&lt;code&gt;resolvedXml&lt;/code&gt; contains &lt;strong&gt;both&lt;/strong&gt; files' contents. An attacker doesn't need to route a read through &lt;code&gt;&amp;lt;note&amp;gt;&lt;/code&gt;&lt;br&gt;
at all — anywhere in the document is enough, and several files can leak in a single request.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 5 — Attempting the platform's own Active Test probe — and finding there isn't one
&lt;/h2&gt;

&lt;p&gt;The platform has two dozen wired Active Test probes: SSTI, GraphQL introspection, JWT verification, mass&lt;br&gt;
assignment, CSRF confirmation, and more. I checked the full checklist and the CLI's &lt;code&gt;--probes&lt;/code&gt; alias list&lt;br&gt;
on camera rather than assert it from memory — neither has an XXE entry. Not a blind or limited probe like&lt;br&gt;
SSRF's — genuinely absent. That's the whole phase: confirming an absence is itself the useful result here,&lt;br&gt;
not a consolation prize for a failed run.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 6 — Filing the bug's only finding, by hand
&lt;/h2&gt;

&lt;p&gt;With nothing in the pipeline ever creating a &lt;code&gt;Finding&lt;/code&gt; object for this bug, the only way it exists in the&lt;br&gt;
platform at all is filing it manually through the Assets hub's "Create finding" tool. Worth knowing before&lt;br&gt;
you hit it yourself: that dialog collects a title, a risk level, and free-text evidence — &lt;strong&gt;no CWE or&lt;br&gt;
reference field at all&lt;/strong&gt; — so the CWE-611 citation has to go into the title text by hand. It's also worth&lt;br&gt;
knowing the resulting finding will always show a "no proof of concept" submission blocker, regardless of&lt;br&gt;
how much evidence you paste in — manual findings never populate the platform's structured proof field,&lt;br&gt;
only free text. Cosmetic, not a real block on a normal report export.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 7 — An unexpected second corroboration
&lt;/h2&gt;

&lt;p&gt;Running the platform's generic "Analyze response" tool over the &lt;code&gt;/etc/passwd&lt;/code&gt; variant's captured&lt;br&gt;
transaction surfaced something worth keeping: alongside the usual generic header findings, a completely&lt;br&gt;
unrelated rule — a generic filesystem-path-disclosure check — independently flagged the same response,&lt;br&gt;
because it literally contains an absolute path string. Different rule, different reasoning, same&lt;br&gt;
underlying bug — a nice bit of corroborating evidence that cost nothing extra to collect.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 8 — A fully offline explanation
&lt;/h2&gt;

&lt;p&gt;With the manually-filed finding's evidence in place, a local LLM generates a plain-language narrative from&lt;br&gt;
it: something like &lt;em&gt;"A hand-rolled XML entity resolver reads an attacker-specified file path from a SYSTEM&lt;br&gt;
entity with no restriction, allowing disclosure of arbitrary files readable by the server process."&lt;/em&gt; No&lt;br&gt;
network call is made — and this works identically whether a finding was auto-created by a rule or filed by&lt;br&gt;
hand, since the narrator only ever reads the finding's own fields, never its provenance.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;The demo's own bug is a hand-rolled resolver that never should have resolved external entities in the&lt;br&gt;
first place — the fix isn't a configuration flag here, it's refusing to read &lt;code&gt;SYSTEM&lt;/code&gt; identifiers at all:&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;resolveEntities&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;)&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="sr"&gt;/&amp;lt;!DOCTYPE/i&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;xml&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOCTYPE declarations are not permitted in submitted XML&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="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;xml&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="c1"&gt;// no external entities are ever resolved&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For a real XML parser, the equivalent is a configuration setting rather than a rewrite: disable DTD&lt;br&gt;
processing and external-entity resolution outright (most real parsers have an explicit&lt;br&gt;
&lt;code&gt;disallow-doctype-decl&lt;/code&gt; / "external entities off" setting — e.g. &lt;code&gt;libxmljs&lt;/code&gt;'s &lt;code&gt;parseXml(xml, { noent:&lt;br&gt;
false, dtdload: false, dtdvalid: false })&lt;/code&gt;, or Node's &lt;code&gt;fast-xml-parser&lt;/code&gt; with &lt;code&gt;processEntities: false&lt;/code&gt;), or&lt;br&gt;
use a parser that doesn't support DTDs at all for untrusted input — verbatim the fix the demo app's own&lt;br&gt;
&lt;code&gt;README.md&lt;/code&gt; suggests.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's honestly &lt;em&gt;not&lt;/em&gt; covered
&lt;/h2&gt;

&lt;p&gt;This is the starkest version of "not covered" in the whole series: no static match, no dynamic/business-&lt;br&gt;
logic match, no Active Test probe at all. That's not three separate failures — it's one honest gap,&lt;br&gt;
demonstrated on camera at every layer rather than glossed over. The static rule's precision (it targets a&lt;br&gt;
real library's real API, just not this app's) is actually the most reassuring part: it isn't guessing,&lt;br&gt;
it's correctly scoped, and this app simply falls outside that scope by design.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The target app and the full step-by-step playbook (every click, every payload, every panel, plus the exact&lt;br&gt;
reasoning behind each "no finding" result) are linked below if you want to reproduce this end-to-end&lt;br&gt;
against your own local copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is an intentionally vulnerable local training app.&lt;/strong&gt; Never run these techniques against a&lt;br&gt;
system you don't own or don't have explicit written authorization to test.&lt;/p&gt;

&lt;p&gt;Try it yourself → &lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;br&gt;
Windows Store &lt;a href="https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US" rel="noopener noreferrer"&gt;https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US&lt;/a&gt;&lt;br&gt;
Web Site &lt;a href="https://Sendwavehub.tech/en/apps/ai-security-studio-4" rel="noopener noreferrer"&gt;https://Sendwavehub.tech/en/apps/ai-security-studio-4&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Server-Side Request Forgery (CWE-918): From Auto-Detected Finding to Live Exploit to an Honest Tooling Gap — Fully Offline</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Tue, 01 Sep 2026 04:47:35 +0000</pubDate>
      <link>https://dev.to/jomynn/server-side-request-forgery-cwe-918-from-auto-detected-finding-to-live-exploit-to-an-honest-57d6</link>
      <guid>https://dev.to/jomynn/server-side-request-forgery-cwe-918-from-auto-detected-finding-to-live-exploit-to-an-honest-57d6</guid>
      <description>&lt;p&gt;A full walkthrough of finding, proving, and reporting a real SSRF bug with a deterministic rule engine, a live form-based exploit, an attempted (and explained) automated probe, and a fully offline local LLM — no cloud AI anywhere in the pipeline.&lt;br&gt;
tags: security, javascript, node, webdev&lt;/p&gt;

&lt;p&gt;One form field. One "internal-only" endpoint that was never supposed to be reachable. Here's the&lt;br&gt;
full story of a real &lt;a href="https://cwe.mitre.org/data/definitions/918.html" rel="noopener noreferrer"&gt;CWE-918&lt;/a&gt; (Server-Side Request&lt;br&gt;
Forgery) vulnerability, from the moment a rule engine flags it, through a form-only live exploit, to&lt;br&gt;
an honest look at where the platform's own automated probe hits a real limit — all running locally,&lt;br&gt;
with no cloud AI anywhere in the pipeline.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/29LSHI5Jg50" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The vulnerable code
&lt;/h2&gt;

&lt;p&gt;The target is a deliberately vulnerable local training app: a "URL preview" tool that fetches&lt;br&gt;
whatever URL you give it, server-side.&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;url&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;url is required&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;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;follow&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// BUG: no host/scheme allowlist&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;text&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;2000&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;&lt;code&gt;url&lt;/code&gt; comes straight from the request body and goes straight into &lt;code&gt;fetch()&lt;/code&gt; — no allowlist on scheme&lt;br&gt;
or destination host. The same server also exposes &lt;code&gt;/internal/admin-status&lt;/code&gt;, an "internal-only"&lt;br&gt;
endpoint that isn't linked from any page and simulates something like a cloud-metadata endpoint or an&lt;br&gt;
admin panel. &lt;code&gt;/preview&lt;/code&gt; can reach it anyway — and because the response body gets reflected straight&lt;br&gt;
back to the caller, this isn't blind SSRF: the internal endpoint's own flag comes back in the JSON,&lt;br&gt;
a self-contained proof with nothing but a form submission.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — A deterministic rule engine catches it, not an LLM
&lt;/h2&gt;

&lt;p&gt;Before any AI touches this finding, the same taint-tracking rule that flags command injection and&lt;br&gt;
path traversal also declares an outbound-request sink, and walks the exact shape this bug takes:&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;     &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;untrusted&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                 &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="nx"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;outbound&lt;/span&gt; &lt;span class="nx"&gt;HTTP&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One static scan raises a &lt;strong&gt;High&lt;/strong&gt;, &lt;strong&gt;CWE-918&lt;/strong&gt; finding automatically — zero manual finding-creation,&lt;br&gt;
zero guesswork. The finding already carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — the matched source line, with the exact column of the injection point pinned inside
the tainted expression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Level&lt;/strong&gt; — High&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Reference&lt;/strong&gt; — CWE-918 / OWASP A10:2021 (Server-Side Request Forgery)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule engine discovers; the LLM only ever explains an &lt;em&gt;already-verified&lt;/em&gt; finding afterward. It&lt;br&gt;
never gets to invent a vulnerability on its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — Proving it live, with zero extra tooling
&lt;/h2&gt;

&lt;p&gt;The form field is prefilled &lt;code&gt;http://localhost:3007/internal/admin-status&lt;/code&gt;. Submitting it returns:&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;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"http://localhost:3007/internal/admin-status"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"body"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;internal&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:true,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;message&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;This endpoint should only ever be called by trusted internal callers.&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;,&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;fakeSecret&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;:&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;FLAG{ssrf-reached-internal-endpoint}&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&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;&lt;code&gt;body&lt;/code&gt; proves the server, not the browser, reached an endpoint that "isn't linked anywhere and isn't&lt;br&gt;
meant to be reachable from outside this server." One request/response pair is already enough evidence&lt;br&gt;
on its own — no server access needed to confirm the bug, because the app's own response is externally&lt;br&gt;
observable proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Escalating the payload (and what a rejection does and doesn't prove)
&lt;/h2&gt;

&lt;p&gt;The request body is &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;, but the payload itself has no &lt;code&gt;&amp;amp;&lt;/code&gt;/&lt;code&gt;=&lt;/code&gt;&lt;br&gt;
characters, so none of command injection's body-splitting trap applies here — values load and send as&lt;br&gt;
typed:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;What it proves&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url=http://127.0.0.1:3007/internal/admin-status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Loopback literal instead of &lt;code&gt;localhost&lt;/code&gt; — same flag comes back&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url=http://[::1]:3007/internal/admin-status&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;IPv6 loopback — a third distinct way into the same endpoint, ruling out a naive hostname-string allowlist&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;url=file:///etc/passwd&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Rejected with a 502 — but by Node's own &lt;code&gt;fetch&lt;/code&gt; (undici), which only supports &lt;code&gt;http&lt;/code&gt;/&lt;code&gt;https&lt;/code&gt;. That's a runtime-level restriction, not an application-level allowlist, and shouldn't be mistaken for one in a write-up&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Three host variations, one important negative result — and the negative result is worth writing up&lt;br&gt;
carefully instead of skipping past it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — Attempting the platform's own Active Test probe — and explaining why it can't confirm this one
&lt;/h2&gt;

&lt;p&gt;The platform does have a real, wired SSRF probe — a GUI checkbox and a CLI flag, backed by an&lt;br&gt;
offline, local out-of-band collaborator (no third-party service, no internet). I set one up and ran&lt;br&gt;
it against &lt;code&gt;/preview&lt;/code&gt;'s &lt;code&gt;url&lt;/code&gt; parameter anyway, on camera, rather than just asserting it wouldn't&lt;br&gt;
work.&lt;/p&gt;

&lt;p&gt;It came back with no finding — and the reason is architectural, not a fluke: the probe always injects&lt;br&gt;
its callback URL via a &lt;strong&gt;GET query string&lt;/strong&gt; (&lt;code&gt;ActiveTestUrls.WithQueryParameter&lt;/code&gt;, sent through a&lt;br&gt;
GET-only client call). This app's vulnerable parameter exists &lt;strong&gt;only&lt;/strong&gt; on &lt;code&gt;POST /preview&lt;/code&gt;'s form&lt;br&gt;
body — there's no &lt;code&gt;GET /preview&lt;/code&gt; route at all, so the probe's request 404s before the server ever&lt;br&gt;
calls &lt;code&gt;fetch()&lt;/code&gt;. No collaborator misconfiguration, no false negative on the app's actual exposure —&lt;br&gt;
just a probe that was built for a different injection channel than this specific endpoint uses.&lt;/p&gt;

&lt;p&gt;Two findings would have been the tidy outcome. One finding plus a documented, code-level reason for&lt;br&gt;
why the second attempt didn't land is the honest one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — A fully offline explanation
&lt;/h2&gt;

&lt;p&gt;With the static evidence and the manual proof both attached to the same finding, a local LLM&lt;br&gt;
generates a plain-language narrative from the structured evidence: something like &lt;em&gt;"Untrusted input&lt;br&gt;
reaches an outbound &lt;code&gt;fetch()&lt;/code&gt; call with no host/scheme allowlist, letting the server be used to reach&lt;br&gt;
its own internal-only endpoints."&lt;/em&gt; No network call is made. The model reasons over an&lt;br&gt;
already-verified finding; it doesn't discover anything new.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6 — The fix
&lt;/h2&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;ALLOWED_HOSTS&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;Set&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;api.trusted-partner.example&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/preview&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;target&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;url&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;catch&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;invalid url&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="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="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;http:&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="s1"&gt;https:&lt;/span&gt;&lt;span class="dl"&gt;'&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;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;protocol&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;ALLOWED_HOSTS&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hostname&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;destination not allowed&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;redirect&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;manual&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt; &lt;span class="c1"&gt;// no automatic redirect to an unchecked host&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;upstream&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;status&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;Allowlist the destination hosts and schemes, resolve and re-validate the actual address before&lt;br&gt;
connecting (reject loopback/link-local/private ranges), turn off automatic redirect-following (or&lt;br&gt;
re-validate the redirect target), and isolate the fetcher's own network egress as defense in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's honestly &lt;em&gt;not&lt;/em&gt; covered
&lt;/h2&gt;

&lt;p&gt;The Active Test tab's SSRF probe is real, wired, and wouldn't be a no-op against every SSRF bug —&lt;br&gt;
just this specific POST-body-only shape. That's a narrower gap than command injection's "no GUI/CLI&lt;br&gt;
entry point at all," but it's still a gap, and I'd rather show it failing on camera with the reason&lt;br&gt;
attached than imply the platform caught two independent confirmations here when it only caught one.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The target app and the full step-by-step playbook (every click, every payload, every panel) are&lt;br&gt;
linked below if you want to reproduce this end-to-end against your own local copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is an intentionally vulnerable local training app.&lt;/strong&gt; Never run these techniques against a&lt;br&gt;
system you don't own or don't have explicit written authorization to test.&lt;/p&gt;

&lt;p&gt;Try it yourself → &lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;br&gt;
Windows Store &lt;a href="https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US" rel="noopener noreferrer"&gt;https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US&lt;/a&gt;&lt;br&gt;
Web Site &lt;a href="https://Sendwavehub.tech/en/apps/ai-security-studio-4" rel="noopener noreferrer"&gt;https://Sendwavehub.tech/en/apps/ai-security-studio-4&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Path traversal caught two ways — static analysis and a live probe, no cloud AI</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Sat, 29 Aug 2026 08:43:05 +0000</pubDate>
      <link>https://dev.to/jomynn/path-traversal-caught-two-ways-static-analysis-and-a-live-probe-no-cloud-ai-eib</link>
      <guid>https://dev.to/jomynn/path-traversal-caught-two-ways-static-analysis-and-a-live-probe-no-cloud-ai-eib</guid>
      <description>&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Here's the route from a small Express demo app&lt;br&gt;
(&lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;&lt;code&gt;scan-target-demo-apps/apps/06-path-traversal&lt;/code&gt;&lt;/a&gt;):&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/files&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;name&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&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;filePath&lt;/span&gt; &lt;span class="o"&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;docsDir&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nx"&gt;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;readFile&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;filePath&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;utf8&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;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&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="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;404&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;file not found&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;path&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;filePath&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;type&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;text/plain&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&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;&lt;code&gt;name&lt;/code&gt; comes straight from the query string and goes straight into &lt;code&gt;path.join()&lt;/code&gt; — no check that the&lt;br&gt;
resolved path actually stays inside &lt;code&gt;docsDir&lt;/code&gt;. &lt;code&gt;path.join('./docs', '../secret.txt')&lt;/code&gt; happily&lt;br&gt;
resolves to a file &lt;em&gt;outside&lt;/em&gt; &lt;code&gt;./docs&lt;/code&gt;. Request &lt;code&gt;/files?name=../secret.txt&lt;/code&gt; and the server reads and&lt;br&gt;
returns whatever's sitting one directory above the one it's supposed to be confined to.&lt;/p&gt;

&lt;p&gt;Classic CWE-22, OWASP A01:2021 (Broken Access Control). Nothing novel in the bug itself — the&lt;br&gt;
interesting part, again, is what caught it and how many independent ways.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually caught it — twice, independently
&lt;/h2&gt;

&lt;p&gt;We ran this target through &lt;strong&gt;AI Security Studio&lt;/strong&gt;, an offline security research platform we've been&lt;br&gt;
building. No cloud calls, no code leaves the machine.&lt;/p&gt;

&lt;p&gt;This is the first target in the series where the bug is confirmed by &lt;strong&gt;both&lt;/strong&gt; ends of the pipeline&lt;br&gt;
at once, not just one:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;1. Static — before anyone opens the app.&lt;/strong&gt; The same JavaScript taint rule that flags command&lt;br&gt;
injection also declares a filesystem sink: &lt;code&gt;fs.readFile&lt;/code&gt; / &lt;code&gt;readFileSync&lt;/code&gt; / &lt;code&gt;writeFile&lt;/code&gt; /&lt;br&gt;
&lt;code&gt;.sendFile(&lt;/code&gt;, etc. It traces &lt;code&gt;req.query.name&lt;/code&gt; → &lt;code&gt;path.join(docsDir, name)&lt;/code&gt; → &lt;code&gt;fs.readFile(filePath,&lt;br&gt;
…)&lt;/code&gt; entirely inside &lt;code&gt;server.js&lt;/code&gt;, and auto-creates a finding — Risk: High, CWE-22, OWASP A01:2021 —&lt;br&gt;
with zero manual steps. This scores &lt;em&gt;reachability&lt;/em&gt;: untrusted input reaches a filesystem sink.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. Dynamic — a live, automated probe.&lt;/strong&gt; Unlike some other bug classes in this series where the&lt;br&gt;
active-probe side isn't wired up yet, path traversal has a real, on-by-default probe&lt;br&gt;
(&lt;code&gt;PathTraversalProbeAdapter&lt;/code&gt;, &lt;code&gt;ProbeSafetyTier.ReadOnly&lt;/code&gt;). Point it at &lt;code&gt;/files&lt;/code&gt; with parameter &lt;code&gt;name&lt;/code&gt;&lt;br&gt;
and run it: it comes back with a &lt;em&gt;Critical&lt;/em&gt; finding, &lt;code&gt;Confidence: Likely&lt;/code&gt;, and — the part that&lt;br&gt;
usually takes a human write-up — an attached, ready-to-run &lt;code&gt;curl&lt;/code&gt; reproduction already generated. This&lt;br&gt;
scores &lt;em&gt;confirmed exploitation&lt;/em&gt;, which is why it lands at a higher risk level than the static-only&lt;br&gt;
finding for the same root cause. Different numbers for the same bug at different pipeline stages is&lt;br&gt;
expected, not a bug in the platform.&lt;/p&gt;

&lt;p&gt;Proving it by hand needs nothing but a browser, too — no tooling required:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight http"&gt;&lt;code&gt;&lt;span class="err"&gt;GET /files?name=../secret.txt
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;returns the contents of &lt;code&gt;secret.txt&lt;/code&gt;, a file that lives one directory above the app's intended&lt;br&gt;
&lt;code&gt;./docs&lt;/code&gt; root and was never meant to be reachable through &lt;code&gt;/files&lt;/code&gt; at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why "offline" is the actual point
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;You can point this at unreleased/internal code without a vendor's cloud pipeline ever touching it.&lt;/li&gt;
&lt;li&gt;The rule engine and the active-test probe — not the LLM — do the actual finding and the actual
exploitation. Results are reproducible and don't drift between runs.&lt;/li&gt;
&lt;li&gt;The local LLM's job shrinks to something it's actually reliable at: turning the already-structured
evidence (the tainted source line, the curl proof, the CWE/OWASP references) into a plain-language
explanation — "untrusted input reaches a filesystem read via unsanitized &lt;code&gt;path.join&lt;/code&gt;, allowing
arbitrary file disclosure outside the intended directory." No raw request/response bodies, no raw
source files, ever get handed to the model directly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The demo app is intentionally vulnerable and open source — safe to point any scanner at, including&lt;br&gt;
this one:&lt;/p&gt;

&lt;p&gt;Try it yourself → &lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;br&gt;
Windows Store &lt;a href="https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US" rel="noopener noreferrer"&gt;https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US&lt;/a&gt;&lt;br&gt;
Web Site &lt;a href="https://Sendwavehub.tech/en/apps/ai-security-studio-4" rel="noopener noreferrer"&gt;https://Sendwavehub.tech/en/apps/ai-security-studio-4&lt;/a&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Video walkthrough:   &lt;iframe src="https://www.youtube.com/embed/MUvFcqqAKvY" width="710" height="399"&gt;
  &lt;/iframe&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Built for security researchers, pentesters, and AppSec teams doing work they're authorized to do.&lt;/p&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>webdev</category>
      <category>node</category>
    </item>
    <item>
      <title>Command Injection (CWE-78): From Auto-Detected Finding to Live Exploit to Fixed — Fully Offline</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Thu, 27 Aug 2026 09:53:43 +0000</pubDate>
      <link>https://dev.to/jomynn/command-injection-cwe-78-from-auto-detected-finding-to-live-exploit-to-fixed-fully-offline-573c</link>
      <guid>https://dev.to/jomynn/command-injection-cwe-78-from-auto-detected-finding-to-live-exploit-to-fixed-fully-offline-573c</guid>
      <description>&lt;p&gt;One tainted variable. One shell on the host. Here's the full story of a real&lt;br&gt;
&lt;a href="https://cwe.mitre.org/data/definitions/78.html" rel="noopener noreferrer"&gt;CWE-78&lt;/a&gt; (OS Command&lt;br&gt;
Injection) vulnerability, from the moment a rule engine flags it to a&lt;br&gt;
disclosure-ready report — all running locally, with no cloud AI anywhere in&lt;br&gt;
the pipeline.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/cOGX3LxZoHw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The vulnerable code
&lt;/h2&gt;

&lt;p&gt;The target is a deliberately vulnerable local training app: a tiny "network&lt;br&gt;
diagnostics" tool that pings a host you give it.&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ping&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&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;cmd&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`ping -c 1 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&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="c1"&gt;// BUG: unsanitized string concatenation&lt;/span&gt;
  &lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No validation. No escaping. No argument array — just user input flowing&lt;br&gt;
straight into a shell. And because the JSON response echoes back the exact&lt;br&gt;
&lt;code&gt;cmd&lt;/code&gt; string the server built, the vulnerable string is visible in &lt;em&gt;every&lt;/em&gt;&lt;br&gt;
response, not just the source — one of the cleanest bugs you'll ever capture&lt;br&gt;
airtight evidence from.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 1 — A deterministic rule engine catches it, not an LLM
&lt;/h2&gt;

&lt;p&gt;Before any AI touches this finding, a taint-tracking static analysis rule&lt;br&gt;
walks the exact shape this bug takes:&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;    &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="nx"&gt;source&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;untrusted&lt;/span&gt; &lt;span class="nx"&gt;request&lt;/span&gt; &lt;span class="nx"&gt;value&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;cmd&lt;/span&gt;  &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`ping -c 1 &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;  &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="nx"&gt;assignment&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;taint&lt;/span&gt; &lt;span class="nx"&gt;propagates&lt;/span&gt; &lt;span class="nx"&gt;through&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="err"&gt;→&lt;/span&gt; &lt;span class="nx"&gt;cmd&lt;/span&gt;
&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cmd&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="err"&gt;…&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;                     &lt;span class="err"&gt;→&lt;/span&gt;  &lt;span class="nx"&gt;sink&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;child_process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;exec&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One static scan raises a &lt;strong&gt;Critical&lt;/strong&gt;, &lt;strong&gt;CWE-78&lt;/strong&gt; finding automatically —&lt;br&gt;
zero manual finding-creation, zero guesswork. The finding already carries:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Evidence&lt;/strong&gt; — the matched source line, with the exact column of the
injection point pinned inside the tainted expression&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Risk Level&lt;/strong&gt; — Critical&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Security Reference&lt;/strong&gt; — CWE-78 / OWASP A03:2021 (Injection)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The rule engine discovers; the LLM only ever explains an &lt;em&gt;already-verified&lt;/em&gt;&lt;br&gt;
finding afterward. It never gets to invent a vulnerability on its own.&lt;/p&gt;
&lt;h2&gt;
  
  
  Step 2 — Proving it live, with zero extra tooling
&lt;/h2&gt;

&lt;p&gt;The form field is prefilled &lt;code&gt;127.0.0.1; id&lt;/code&gt;. Submitting it returns:&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;"cmd"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ping -c 1 127.0.0.1; id"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"stdout"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"PING 127.0.0.1 ...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&gt;uid=501(demo) gid=20(staff) groups=...&lt;/span&gt;&lt;span class="se"&gt;\n&lt;/span&gt;&lt;span class="s2"&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;"stderr"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&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;"error"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="kc"&gt;null&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;&lt;code&gt;cmd&lt;/code&gt; proves the server built exactly the string you'd expect from unescaped&lt;br&gt;
concatenation. &lt;code&gt;stdout&lt;/code&gt; proves it further — after the ping output, a&lt;br&gt;
&lt;code&gt;uid=…gid=…&lt;/code&gt; line: the output of &lt;code&gt;id&lt;/code&gt;, a command nobody asked the app to&lt;br&gt;
run. One request/response pair is already enough evidence on its own — no&lt;br&gt;
server access needed to confirm the bug, because the app's own response is&lt;br&gt;
externally observable proof.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3 — Varying the payload (and why encoding matters)
&lt;/h2&gt;

&lt;p&gt;A quick gotcha worth calling out for anyone testing this class of bug by&lt;br&gt;
hand: the request's &lt;code&gt;Content-Type&lt;/code&gt; is &lt;code&gt;application/x-www-form-urlencoded&lt;/code&gt;,&lt;br&gt;
so a literal &lt;code&gt;host=127.0.0.1 &amp;amp;&amp;amp; whoami&lt;/code&gt; gets split by the form parser on the&lt;br&gt;
raw &lt;code&gt;&amp;amp;&lt;/code&gt; &lt;em&gt;before&lt;/em&gt; the payload ever reaches &lt;code&gt;host&lt;/code&gt; — the metacharacter never&lt;br&gt;
survives. Percent-encode it instead:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Payload&lt;/th&gt;
&lt;th&gt;Encoded body&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;127.0.0.1 &amp;amp;&amp;amp; whoami&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;&lt;code&gt;host=127.0.0.1%20%26%26%20whoami&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;`127.0.0.1 \&lt;/td&gt;
&lt;td&gt;uname -a`&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;`id`&lt;/code&gt; (substitution)&lt;/td&gt;
&lt;td&gt;&lt;code&gt;host=%60id%60&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four independent shell metacharacters (&lt;code&gt;;&lt;/code&gt;, &lt;code&gt;&amp;amp;&amp;amp;&lt;/code&gt;, &lt;code&gt;|&lt;/code&gt;, &lt;code&gt;`&lt;/code&gt;), four&lt;br&gt;
independent proofs that this reaches a real shell — not one lucky payload.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4 — A fully offline explanation
&lt;/h2&gt;

&lt;p&gt;With the static evidence and the live proof both attached to the same&lt;br&gt;
finding, a local LLM generates a plain-language narrative from the&lt;br&gt;
structured evidence — something like &lt;em&gt;"Untrusted input reaches&lt;br&gt;
&lt;code&gt;child_process.exec&lt;/code&gt; via string concatenation — full remote code&lt;br&gt;
execution."&lt;/em&gt; No network call is made. The model reasons over an&lt;br&gt;
already-verified finding; it doesn't discover anything new.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5 — The fix
&lt;/h2&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;execFile&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node:child_process&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/ping&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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;host&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;host&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;127.0.0.1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&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="s1"&gt;ping&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="s1"&gt;-c&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="s1"&gt;1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;host&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;5000&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&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="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;stderr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&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;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 argument array means there's no shell to interpret in the&lt;br&gt;
first place — &lt;code&gt;host&lt;/code&gt; is passed as a literal argument, never concatenated&lt;br&gt;
into a command string. Add strict input validation on &lt;code&gt;host&lt;/code&gt; (e.g. an&lt;br&gt;
IP/hostname allow-list pattern) as defense in depth.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's honestly &lt;em&gt;not&lt;/em&gt; covered
&lt;/h2&gt;

&lt;p&gt;Two active-exploitation probes for this bug class exist in the platform's&lt;br&gt;
engine, but neither is currently wired to a GUI checkbox or CLI flag — so&lt;br&gt;
dynamic confirmation via automated active testing isn't possible today for&lt;br&gt;
this specific bug. That's exactly why this walkthrough proves the exploit&lt;br&gt;
by hand instead of relying on tooling that isn't there yet. Calling out&lt;br&gt;
tooling gaps honestly matters more to me than pretending coverage is&lt;br&gt;
complete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The target app and the full step-by-step playbook (every click, every&lt;br&gt;
payload, every panel) are linked below if you want to reproduce this&lt;br&gt;
end-to-end against your own local copy.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;This is an intentionally vulnerable local training app.&lt;/strong&gt; Never run these&lt;br&gt;
techniques against a system you don't own or don't have explicit written&lt;br&gt;
authorization to test.&lt;/p&gt;

&lt;p&gt;Try it yourself → &lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;br&gt;
Windows Store &lt;a href="https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US" rel="noopener noreferrer"&gt;https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US&lt;/a&gt;&lt;br&gt;
Web Site &lt;a href="https://Sendwavehub.tech/en/apps/ai-security-studio-4" rel="noopener noreferrer"&gt;https://Sendwavehub.tech/en/apps/ai-security-studio-4&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>javascript</category>
      <category>node</category>
      <category>webdev</category>
    </item>
    <item>
      <title>Manually Breaking Authentication — A Full Walkthrough (CWE-307, CWE-330, CWE-640)</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Wed, 26 Aug 2026 06:14:15 +0000</pubDate>
      <link>https://dev.to/jomynn/manually-breaking-authentication-a-full-walkthrough-cwe-307-cwe-330-cwe-640-305c</link>
      <guid>https://dev.to/jomynn/manually-breaking-authentication-a-full-walkthrough-cwe-307-cwe-330-cwe-640-305c</guid>
      <description>&lt;p&gt;A fully manual security assessment of three broken-authentication bugs — no scanners, just a proxy-backed browser and raw HTTP evidence, mapped end-to-end to CWE and OWASP.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Educational / authorized testing only.&lt;/strong&gt; Everything below targets a local, deliberately vulnerable training application, not a live system. Never run these techniques against anything you don't own or don't have explicit written authorization to test.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Automated scanners are great at pattern-matching known vulnerability classes. They're much weaker at authentication flaws that look completely legitimate at the HTTP level — a 200, a 401, a token — and only become a bug once you understand &lt;em&gt;how&lt;/em&gt; the value was generated. This walkthrough is a fully manual assessment of three such bugs in a deliberately vulnerable login system, captured start to finish with nothing but a proxy-backed browser and a network tab.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/uKYHn1MWrn0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The bugs
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;#&lt;/th&gt;
&lt;th&gt;Bug&lt;/th&gt;
&lt;th&gt;CWE&lt;/th&gt;
&lt;th&gt;OWASP&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;POST /login&lt;/code&gt; has no rate limit, delay, or lockout&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cwe.mitre.org/data/definitions/307.html" rel="noopener noreferrer"&gt;CWE-307&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A07:2021 – Identification and Authentication Failures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Session tokens are sequential integers, not random&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cwe.mitre.org/data/definitions/330.html" rel="noopener noreferrer"&gt;CWE-330&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A07:2021&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Password-reset token is &lt;code&gt;md5(username)&lt;/code&gt; — deterministic, not random or single-use&lt;/td&gt;
&lt;td&gt;&lt;a href="https://cwe.mitre.org/data/definitions/640.html" rel="noopener noreferrer"&gt;CWE-640&lt;/a&gt;&lt;/td&gt;
&lt;td&gt;A07:2021&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Phase 1 — No brute-force protection
&lt;/h2&gt;

&lt;p&gt;Three wrong passwords for &lt;code&gt;admin&lt;/code&gt;, back to back, then the real one. All four &lt;code&gt;POST /login&lt;/code&gt; requests return instantly — no &lt;code&gt;429&lt;/code&gt;, no CAPTCHA, no increasing delay. Filtering the network tab down to &lt;code&gt;login&lt;/code&gt; shows the whole sequence sitting there in plain sight: nothing on the server side ever notices the repetition.&lt;/p&gt;

&lt;p&gt;That's the entire proof for CWE-307. A real attacker isn't limited to four guesses — they're limited by whatever rate their network connection allows, because the server places no limit of its own.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 2 — Predictable session tokens
&lt;/h2&gt;

&lt;p&gt;The login endpoint hands out tokens from a shared, incrementing counter — regardless of which account is logging in. &lt;code&gt;alice&lt;/code&gt; gets &lt;code&gt;1001&lt;/code&gt;. &lt;code&gt;bob&lt;/code&gt; gets &lt;code&gt;1002&lt;/code&gt;. That's already suspicious, but the real proof of impact is this: clear your cookies, open a private window, and request &lt;code&gt;/whoami?session=1001&lt;/code&gt; with &lt;strong&gt;zero credentials&lt;/strong&gt;. The server hands back &lt;code&gt;alice&lt;/code&gt;'s full identity, because "knowing a number" and "being alice" are treated as equivalent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Phase 3 — A reset token that isn't a secret
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;/reset-token?username=alice&lt;/code&gt; returns a 32-character hex string that looks random — until you request it again and get the &lt;em&gt;exact same value&lt;/em&gt; back. It's not a secret; it's &lt;code&gt;md5("alice")&lt;/code&gt;, a pure function of public data. Anyone who knows a username can compute their "reset token" offline, with no server interaction and no account access at all.&lt;/p&gt;

&lt;h2&gt;
  
  
  From evidence to finding
&lt;/h2&gt;

&lt;p&gt;Each bug was routed from the network capture into a manual finding — evidence field populated with the actual request/response pairs, CWE and OWASP mapping attached, and a concrete fix:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Finding A&lt;/strong&gt; (Medium) — add per-account and per-IP throttling with exponential backoff or lockout, plus a CAPTCHA after repeated failures.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finding B&lt;/strong&gt; (High) — issue tokens from a cryptographically secure random source (128+ bits), never a counter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Finding C&lt;/strong&gt; (High) — generate a random, single-use, time-limited reset token, stored server-side and delivered only to the account's verified contact method.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Takeaway
&lt;/h2&gt;

&lt;p&gt;None of this required exploit tooling — it required patience, a network tab, and the discipline to capture evidence before writing a single conclusion. Scanners are excellent at known-signature bugs; deterministic secrets and predictable state are exactly the class of finding that still needs a human reading raw traffic.&lt;/p&gt;

&lt;p&gt;What's the last "boring" auth bug your team caught that a scanner missed? Curious to hear how others are covering this gap.&lt;/p&gt;

&lt;p&gt;Try it yourself → &lt;a href="https://dev.tourl"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;br&gt;
Windows Store &lt;a href="https://dev.tourl"&gt;https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US&lt;/a&gt;&lt;br&gt;
Web Site &lt;a href="https://dev.tourl"&gt;https://Sendwavehub.tech/en/apps/ai-security-studio-4&lt;/a&gt;&lt;/p&gt;

</description>
      <category>security</category>
      <category>appsec</category>
      <category>testing</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Manually Proving and Documenting an IDOR (CWE-639) — A Full Walkthrough</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Mon, 24 Aug 2026 13:01:03 +0000</pubDate>
      <link>https://dev.to/jomynn/manually-proving-and-documenting-an-idor-cwe-639-a-full-walkthrough-m7m</link>
      <guid>https://dev.to/jomynn/manually-proving-and-documenting-an-idor-cwe-639-a-full-walkthrough-m7m</guid>
      <description>&lt;p&gt;Most IDOR write-ups stop at "change the ID in the URL and you get someone else's data."&lt;br&gt;
That's the easy part. The part that actually matters — proving it rigorously, ruling out&lt;br&gt;
the boring explanation, and turning it into a report someone can act on — usually gets&lt;br&gt;
skipped.&lt;/p&gt;

&lt;p&gt;I recorded a full walkthrough doing exactly that, end to end, against a deliberately&lt;br&gt;
vulnerable local training target.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/8RlBRBQBUAw"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;A simple invoice endpoint, &lt;code&gt;GET /invoices/:id&lt;/code&gt;. It checks that &lt;em&gt;someone&lt;/em&gt; is logged in&lt;br&gt;
(401 if not), but never checks that the logged-in identity actually &lt;em&gt;owns&lt;/em&gt; the invoice&lt;br&gt;
it's returning:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
js
`invoices.find(inv =&amp;gt; inv.id === Number(req.params.id))
// no invoice.owner === session.user check`


Log in as any user, change the number in the URL, read anyone's invoice. Classic
CWE-639 — Authorization Bypass Through User-Controlled Key, and it maps directly to:

OWASP A01:2021 — Broken Access Control
API1:2023 — Broken Object Level Authorization (BOLA)

**What the walkthrough actually covers**
The video isn't just "look, it's broken" — it's the full evidence chain a real assessment
needs:

Timestamp   Phase
0:00    Capture two separate identities (alice, bob)
1:26:19 Prove cross-owner access live, in-browser
2:06:26 Pull raw request/response evidence from the network tab
2:52:04 Run a negative control — unauthenticated request, confirm 401
4:08:06 Confirm independently with an automated IDOR probe
6:12:03 Create a structured finding (CWE/OWASP/API mapping + evidence)
8:12:07 Structure the evidence for review
10:29:00    Export a report
The negative control is the step people skip most often, and it's the one that separates
"broken access control" from "there's just no auth at all" — an important distinction if
you're writing this up for a real triager.

**Why the control matters**
Without it, you've only shown that an authenticated user can read someone else's data —
you haven't ruled out the (much less interesting) possibility that the endpoint has no
auth check whatsoever. Hitting the same endpoint with zero session and getting a clean
401 proves the app does enforce authentication — it just never enforces ownership.
That's the difference between a one-line "missing auth" bug and a systemic
authorization-model gap.

**Fix**
Load the object, then check ownership before returning it:

`const invoice = invoices.find(inv =&amp;gt; inv.id === Number(req.params.id));
if (!invoice || invoice.owner !== session.user) {
  return res.status(404).end(); // or 403 — avoid leaking existence via 401 vs 403
}`

Every object access needs to be authorized, not just authenticated — that's the whole bug
in one sentence.

Try it yourself → https://github.com/sendwavehub/scan-target-demo-apps
Windows Store https://apps.microsoft.com/detail/9pj0j7bk1m27?hl=en-US
Web Site https://Sendwavehub.tech/en/apps/ai-security-studio-4 
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>api</category>
      <category>cybersecurity</category>
      <category>security</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Building an Offline AI Security Scanner That Doesn't Hallucinate Findings</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Sat, 22 Aug 2026 06:47:26 +0000</pubDate>
      <link>https://dev.to/jomynn/building-an-offline-ai-security-scanner-that-doesnt-hallucinate-findings-3oab</link>
      <guid>https://dev.to/jomynn/building-an-offline-ai-security-scanner-that-doesnt-hallucinate-findings-3oab</guid>
      <description>&lt;p&gt;How AiSec Studio pipes every scan through parser → rule engine → knowledge graph before a local LLM ever sees it — and why that order matters. 60-second Auto Scan demo included.&lt;/p&gt;

&lt;p&gt;Most "AI-powered" security scanners work the same way under the hood: grab the target's code or HTTP responses, stuff them into an LLM prompt, and ask it to find vulnerabilities. It demos well. It also hallucinates constantly — the model pattern-matches on plausible-looking code and reports things that were never exploitable, or never there at all.&lt;/p&gt;

&lt;p&gt;I've been building &lt;strong&gt;AiSec Studio&lt;/strong&gt;, an offline AI security research platform, around the opposite assumption: &lt;strong&gt;the LLM should never be the thing that discovers a vulnerability.&lt;/strong&gt; It should only reason over what a deterministic system already found.&lt;/p&gt;

&lt;p&gt;Here's a 60-second walkthrough of the Auto Scan lane — target URL in, reviewed report out — followed by how the pipeline behind it is actually built.&lt;/p&gt;

&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/xux8-H7e2_0"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The pipeline
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parser → Rule Engine → Knowledge Retrieval → Summarization → LLM → Reasoning → Finding → Report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every stage exists to keep the model away from raw, untrusted input and away from the job of &lt;em&gt;deciding&lt;/em&gt; something is a vulnerability.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. Parse before you analyze
&lt;/h3&gt;

&lt;p&gt;Before any AI touches a target, the code goes through &lt;strong&gt;Roslyn&lt;/strong&gt; (for .NET) or &lt;strong&gt;Tree-sitter&lt;/strong&gt; (everything else) to extract:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;AST&lt;/li&gt;
&lt;li&gt;Call graph / dependency graph&lt;/li&gt;
&lt;li&gt;Controllers, services, routes&lt;/li&gt;
&lt;li&gt;Authentication and authorization wiring&lt;/li&gt;
&lt;li&gt;Configuration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the same reason a compiler front-end exists before an optimizer runs — you don't want probabilistic reasoning operating on unstructured text when a deterministic parser can hand you a structured graph instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Rule engine does the actual discovering
&lt;/h3&gt;

&lt;p&gt;Vulnerability &lt;em&gt;discovery&lt;/em&gt; is a deterministic-rules job, not an LLM job:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Secret detection&lt;/li&gt;
&lt;li&gt;JWT validation (including algorithm-confusion, CWE-347)&lt;/li&gt;
&lt;li&gt;Header / cookie / CSP / CORS analysis&lt;/li&gt;
&lt;li&gt;SQL and XSS pattern detection&lt;/li&gt;
&lt;li&gt;Dependency and configuration analysis&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this requires a model. It requires correct rules, and it's auditable — you can point at exactly which regex or AST pattern fired for a given finding.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Everything lands in a per-engagement Knowledge Graph
&lt;/h3&gt;

&lt;p&gt;Pages, APIs, JS bundles, auth flows, technologies, and findings all get cross-linked by canonical route into one persistent graph per engagement. This is what lets risk &lt;em&gt;propagate&lt;/em&gt; — a leaked JWT secret on one endpoint and an IDOR on a related endpoint aren't two isolated findings, they're connected nodes with compounding risk.&lt;/p&gt;

&lt;p&gt;It also means re-scanning is incremental: unchanged parts of the graph don't get re-analyzed.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. The LLM only reasons over structured summaries — never raw input
&lt;/h3&gt;

&lt;p&gt;This is the boundary that matters most. The local LLM (Qwen2.5-Coder or DeepSeek-Coder, served through Ollama or llama.cpp — no cloud API, ever, at any point) is only handed:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Structured summaries from the rule engine + knowledge graph&lt;/li&gt;
&lt;li&gt;Never raw HTTP responses&lt;/li&gt;
&lt;li&gt;Never raw bundles&lt;/li&gt;
&lt;li&gt;Never raw source dumped wholesale&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Its job is explanation and correlation: &lt;em&gt;why&lt;/em&gt; does this finding matter, &lt;em&gt;what's&lt;/em&gt; the business impact, &lt;em&gt;which&lt;/em&gt; of these twelve findings should a human look at first, &lt;em&gt;how&lt;/em&gt; should the final report read. It doesn't get to originate a finding out of nothing, because it never sees anything a finding could be originated from.&lt;/p&gt;

&lt;h3&gt;
  
  
  5. Confidence tiers instead of confident-sounding guesses
&lt;/h3&gt;

&lt;p&gt;Every finding is tagged:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Confirmed&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Likely&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Possible&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Hypothesis&lt;/strong&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the evidence doesn't support any of those, the system returns &lt;strong&gt;"Needs Manual Verification"&lt;/strong&gt; instead of inventing a conclusion. A finding without evidence, reasoning, a confidence score, a risk level, and a suggested fix isn't a finding — it's a guess wearing a report template.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the demo actually shows
&lt;/h2&gt;

&lt;p&gt;Mapped to the pipeline above, the video is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Auto Scan panel&lt;/strong&gt; — set the target URL&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan Anonymously&lt;/strong&gt; — passive discovery only, rule-engine + parser stage&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scan All&lt;/strong&gt; — runs discovery → analysis → active testing end to end&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Investigate → Finding Report&lt;/strong&gt; — step through findings one by one, each with its evidence and confidence tier attached&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Report → Markdown&lt;/strong&gt; — export the write-up&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Report&lt;/strong&gt; — hand it straight to the OS's default viewer&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Nothing in that flow leaves the machine. No target data, no code, no findings go to a cloud API at any point — that's a hard architectural constraint, not a configuration toggle.&lt;/p&gt;

&lt;p&gt;Try it yourself: &lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;&lt;br&gt;
Free download: &lt;a href="https://apps.microsoft.com/store/detail/9PJ0J7BK1M27?cid=DevShareMCLPCB" rel="noopener noreferrer"&gt;https://apps.microsoft.com/store/detail/9PJ0J7BK1M27?cid=DevShareMCLPCB&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Web download:&lt;br&gt;
&lt;a href="https://sendwavehub.tech/th/apps/ai-security-studio-4" rel="noopener noreferrer"&gt;https://sendwavehub.tech/th/apps/ai-security-studio-4&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>cybersecurity</category>
      <category>llm</category>
      <category>security</category>
    </item>
    <item>
      <title>We caught a SQL injection with an offline AI security scanner — here's the exact query it found</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Thu, 20 Aug 2026 13:57:45 +0000</pubDate>
      <link>https://dev.to/jomynn/we-caught-a-sql-injection-with-an-offline-ai-security-scanner-heres-the-exact-query-it-found-4187</link>
      <guid>https://dev.to/jomynn/we-caught-a-sql-injection-with-an-offline-ai-security-scanner-heres-the-exact-query-it-found-4187</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/GknBmsqaFk4"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;h2&gt;
  
  
  The bug
&lt;/h2&gt;

&lt;p&gt;Here's a login endpoint from a small Express demo app&lt;br&gt;
(&lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;&lt;code&gt;scan-target-demo-apps/apps/01-sql-injection&lt;/code&gt;&lt;/a&gt;):&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="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;username&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="nx"&gt;password&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`SELECT id, username, email, is_admin FROM users
                  WHERE username = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' AND password = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;password&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="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;db&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="nx"&gt;query&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="c1"&gt;// ...&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;You've seen this shape before. &lt;code&gt;username&lt;/code&gt; and &lt;code&gt;password&lt;/code&gt; go straight into the SQL string — no&lt;br&gt;
parameter binding, no escaping. Submit &lt;code&gt;admin' --&lt;/code&gt; as the username and anything as the password,&lt;br&gt;
and the query becomes:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;email&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;is_admin&lt;/span&gt; &lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;users&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;username&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'admin'&lt;/span&gt; &lt;span class="c1"&gt;-- ' AND password = 'anything'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;&lt;code&gt;--&lt;/code&gt; comments out the rest of the line. The password check never runs. &lt;code&gt;{"ok": true}&lt;/code&gt;, logged in&lt;br&gt;
as &lt;code&gt;admin&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Classic CWE-89, OWASP A03:2021. Nothing novel here — the interesting part is what caught it.&lt;/p&gt;
&lt;h2&gt;
  
  
  What actually caught it
&lt;/h2&gt;

&lt;p&gt;We ran this target through &lt;strong&gt;AI Security Studio&lt;/strong&gt;, an offline security research platform we've&lt;br&gt;
been building. No cloud calls, no code leaves the machine — everything you're about to read&lt;br&gt;
happened on a laptop with no network access to anywhere but &lt;code&gt;localhost:3001&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The important design decision: &lt;strong&gt;the LLM never sees raw source code or raw HTTP traffic.&lt;/strong&gt;&lt;br&gt;
The pipeline looks like this:&lt;br&gt;
&lt;/p&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Parser → Rule Engine → Knowledge Retrieval → Summarization → LLM → Reasoning → Finding → Report
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A deterministic parser and rule engine do the actual &lt;em&gt;finding&lt;/em&gt;. The local LLM only explains&lt;br&gt;
&lt;em&gt;already-discovered&lt;/em&gt; evidence — it's not asked to eyeball a codebase and guess where the bugs&lt;br&gt;
might be. If the evidence isn't there, the platform is built to say "Needs Manual Verification"&lt;br&gt;
instead of inventing a conclusion.&lt;/p&gt;

&lt;p&gt;For this run, that meant:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Point the scanner at &lt;code&gt;http://localhost:3001&lt;/code&gt; as scope.&lt;/li&gt;
&lt;li&gt;Let it run a real passive crawl — no exploitation, just discovery.&lt;/li&gt;
&lt;li&gt;Run a source scan against the target's own directory.&lt;/li&gt;
&lt;li&gt;The rule engine flags the string-concatenated query pattern in &lt;code&gt;server.js:56&lt;/code&gt; and the missing
auth check on the state-changing &lt;code&gt;POST /login&lt;/code&gt; route.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated report includes the actual vulnerable line as evidence, not a generic "possible SQL&lt;br&gt;
injection" label:&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="nx"&gt;Source&lt;/span&gt; &lt;span class="nx"&gt;preview&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;
  &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/login&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;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&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="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;username&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="nx"&gt;password&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="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&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;query&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`SELECT id, username, email, is_admin FROM users WHERE username = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;username&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;' AND password = '&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;password&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Risk: High. Root cause: CWE-89 (and a separate CWE-306 finding for the missing authorization&lt;br&gt;
check). Suggested fix: parameterized queries. Confidence explicitly marked — some findings from&lt;br&gt;
this pass are "Confirmed," others are flagged "Needs Manual Verification" rather than asserted,&lt;br&gt;
because header/config-level evidence alone doesn't prove exploitability the way a manual&lt;br&gt;
Active Test / Repeater run does.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why "offline" is the actual point
&lt;/h2&gt;

&lt;p&gt;It's easy to gloss over "runs locally" as a checkbox feature. In practice it means:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;You can point it at unreleased/internal code without a vendor's cloud pipeline ever touching it.&lt;/li&gt;
&lt;li&gt;The rule engine — not the LLM — does secret detection, JWT/header/cookie analysis, and pattern
matching, so results are reproducible and don't drift between runs.&lt;/li&gt;
&lt;li&gt;The LLM's job shrinks to something it's actually reliable at: turning structured findings into a
clear explanation, not hunting for vulnerabilities in raw text.&lt;/li&gt;
&lt;/ul&gt;
&lt;h2&gt;
  
  
  Try it yourself
&lt;/h2&gt;

&lt;p&gt;The demo app is intentionally vulnerable and open source — safe to point any scanner at, including&lt;br&gt;
this one:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Demo target: &lt;a href="https://github.com/sendwavehub/scan-target-demo-apps" rel="noopener noreferrer"&gt;https://github.com/sendwavehub/scan-target-demo-apps&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;AI Security Studio: &lt;a href="https://apps.microsoft.com/store/detail/9PJ0J7BK1M27?cid=DevShareMCLPCB" rel="noopener noreferrer"&gt;https://apps.microsoft.com/store/detail/9PJ0J7BK1M27?cid=DevShareMCLPCB&lt;/a&gt;
&lt;div class="crayons-card c-embed text-styles text-styles--secondary"&gt;
    &lt;div class="c-embed__content"&gt;
        &lt;div class="c-embed__cover"&gt;
          &lt;a href="https://sendwavehub.tech/th/apps/ai-security-studio-4" class="c-link align-middle" rel="noopener noreferrer"&gt;
            &lt;img alt="" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsendwavehub.tech%2Fog-default-blog.png" height="auto" class="m-0"&gt;
          &lt;/a&gt;
        &lt;/div&gt;
      &lt;div class="c-embed__body"&gt;
        &lt;h2 class="fs-xl lh-tight"&gt;
          &lt;a href="https://sendwavehub.tech/th/apps/ai-security-studio-4" rel="noopener noreferrer" class="c-link"&gt;
            SendWaveHub — Hire AI Employees for Your Business Email
          &lt;/a&gt;
        &lt;/h2&gt;
          &lt;p class="truncate-at-3"&gt;
            AI Employees that read, process, and respond to your business email. AI Accountant live now — email marketing, eSign, PDF tools, and automation included. Free plan.
          &lt;/p&gt;
        &lt;div class="color-secondary fs-s flex items-center"&gt;
            &lt;img alt="favicon" class="c-embed__favicon m-0 mr-2 radius-0" src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fsendwavehub.tech%2Fsendwave-icon.svg"&gt;
          sendwavehub.tech
        &lt;/div&gt;
      &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Built for security researchers, pentesters, and AppSec teams doing work they're authorized to do.&lt;/p&gt;

&lt;p&gt;Next up in this series: stored XSS — one comment field, every visitor who loads the page.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>javascript</category>
      <category>security</category>
      <category>sql</category>
    </item>
    <item>
      <title>We Built a Security Scanner That Automates Itself — and Never Touches the Cloud</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Tue, 18 Aug 2026 07:51:55 +0000</pubDate>
      <link>https://dev.to/jomynn/we-built-a-security-scanner-that-automates-itself-and-never-touches-the-cloud-48h6</link>
      <guid>https://dev.to/jomynn/we-built-a-security-scanner-that-automates-itself-and-never-touches-the-cloud-48h6</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/0Dyc07P3AuI"&gt;
  &lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;How AI Security Studio's ASS Script engine lets you record, replay, and narrate a full offline security scan — deterministic analysis first, local LLM reasoning second.&lt;br&gt;
Most "AI-powered" security tools have the same dependency: your code has to leave your machine to get an answer. That's a non-starter for a lot of security research — client engagements under NDA, regulated codebases, or just not wanting proprietary source sitting in someone else's inference logs.&lt;/p&gt;

&lt;p&gt;AI Security Studio is built the other way: &lt;strong&gt;everything runs locally.&lt;/strong&gt; Local LLM (Ollama / llama.cpp / LM Studio), no external API calls, no telemetry. This post walks through one specific piece of it — the automation engine we call &lt;strong&gt;ASS Script&lt;/strong&gt; — and the small offline pipeline I built on top of it to generate narrated, subtitled walkthrough videos without touching a cloud TTS API either.&lt;/p&gt;

&lt;h2&gt;
  
  
  Deterministic first, LLM second
&lt;/h2&gt;

&lt;p&gt;Before any AI touches a finding, it goes through:&lt;/p&gt;

&lt;p&gt;Parser → Rule Engine → Knowledge Retrieval → Summarization → LLM → Reasoning → Finding → Report&lt;/p&gt;

&lt;p&gt;Static parsing (Roslyn / Tree-sitter) and a deterministic rule engine — secret detection, header analysis, JWT validation, SQLi/XSS pattern matching — do the actual discovery. The LLM never sees raw source. It only reasons over structured summaries the rule engine already produced: explaining &lt;em&gt;why&lt;/em&gt; something is a bug, correlating findings, drafting the writeup. If the evidence isn't solid, the system is designed to say "needs manual verification" rather than invent a conclusion.&lt;/p&gt;

&lt;h2&gt;
  
  
  Record once, replay forever
&lt;/h2&gt;

&lt;p&gt;ASS Script (AI Security Studio Automation Script) is an iMacro-style record/play/edit engine built on the same node-graph designer used elsewhere in the app. Scripts are plain YAML — readable and diffable, not an opaque macro blob:&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
yaml
nodes:
  - id: "n5"
    kind: "GuiInteract"
    title: "Click Find bugs on this URL"
    notes: "Imports the URL as a one-host scope. Scope import only — sends no network traffic by itself."
    parameters:
      control: "ScanUrlButton"
      action: "click"
  - id: "n9"
    kind: "GuiInteract"
    title: "Click Scan All"
    notes: "Kicks off the panel's real passive sweep — genuinely live crawl traffic, same as a human operator would trigger."
    parameters:
      control: "ScanAllButton"
      action: "click"
  - id: "n14"
    kind: "RunScan"
    title: "Live source scan"
    notes: "Runs the real ScanService against the target's own source directory — footage shows a real scan, not a mockup."
    parameters:
      path: "/path/to/target/source"
Each node is a real GUI action or a real scan/analyzer call — no simulated steps. You record a workflow once, then replay it identically from the GUI or the CLI (workflow run some-script.adrflow.yaml).

The fun side quest: offline narration for the demo videos
We wanted walkthrough videos with voiceover and subtitles, but the "generate subtitles" step usually means uploading video/audio to a cloud transcription or TTS API — which would be a little embarrassing for a project whose whole pitch is "nothing leaves your machine."

So the pipeline is: write the shot list as a markdown table (beat / timecode / on-screen action / caption / VO line), then run it through a small Python script that:

Parses the table into timed cues
Synthesizes each VO line with macOS's built-in say (fully offline TTS)
Measures each clip's real duration with ffprobe
Concatenates them into one narration track with ffmpeg
Emits a .srt timed to the actual synthesized audio, not a guess
Flags any beat where the narration runs longer than the video's planned window, so you know exactly where to hold a shot longer
The output is a narration.wav + captions.srt pair generated entirely from tools already on the machine — no API keys, no cloud dependency, same philosophy as the scanner itself.

Why this matters beyond the demo video
The pattern generalizes: treat the LLM/AI step as optional and swappable, put the deterministic and local-first parts first. It's true for the security pipeline (rule engine before LLM) and it turned out to be just as true for the tooling around the project (offline TTS before cloud TTS). If your architecture assumes "AI" every time it actually means "network call," that's worth questioning.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>security</category>
      <category>ai</category>
      <category>automation</category>
      <category>opensource</category>
    </item>
    <item>
      <title>🛡️ NPM Safety Guard — All 23 Security Layers Explained</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Mon, 29 Jun 2026 06:20:23 +0000</pubDate>
      <link>https://dev.to/jomynn/npm-safety-guard-all-23-security-layers-explained-1l0</link>
      <guid>https://dev.to/jomynn/npm-safety-guard-all-23-security-layers-explained-1l0</guid>
      <description>&lt;p&gt;  &lt;iframe src="https://www.youtube.com/embed/wd6ZNTIoYKE"&gt;
  &lt;/iframe&gt;
&lt;br&gt;
Every npm project is one malicious package away from a supply-chain breach. NPM Safety Guard catches threats that npm audit completely misses — from DPRK backdoors and typosquatted packages, to exposed API keys and AI credential theft hidden inside your node_modules.&lt;/p&gt;

&lt;p&gt;This video walks through all 23 detection layers, one by one, showing exactly what each layer catches and how it protects your project in real time.&lt;br&gt;
🛡️ Intro&lt;br&gt;
NPM Safety Guard is the most comprehensive npm security scanner for developers. It ships as a VS Code extension (also works in Cursor and Windsurf) and a JetBrains plugin (WebStorm, IntelliJ IDEA, and all IntelliJ-based IDEs). It runs silently in the background and alerts you to supply-chain threats, malware, CVEs, and credential leaks — before they can cause damage.&lt;/p&gt;

&lt;p&gt;Layer 1 — Known Malicious Packages&lt;br&gt;
Checks every package in your package.json against a bundled database of documented supply-chain attacks, including DPRK/Lazarus Group backdoors, the infamous event-stream compromise, and dozens of other confirmed malicious packages. The database is also synced against a live remote feed so newly discovered threats are caught even before you update the extension.&lt;/p&gt;

&lt;p&gt;Layer 2 — CVE Vulnerabilities&lt;br&gt;
Queries the Google OSV.dev API for known CVEs across all your direct dependencies. No API key needed — it is completely free. Results are cached for 24 hours to minimize network calls. CVSS scores are mapped to severity levels (Critical, High, Medium, Low) so you always know exactly how serious each vulnerability is and which version fixes it.&lt;/p&gt;

&lt;p&gt;Layer 3 — Install Script Hooks&lt;br&gt;
Flags packages that declare preinstall, postinstall, install, or prepare npm scripts. These hooks run automatically during npm install — before any of your own code executes — making them the number one real-world vector for supply-chain malware delivery. Legitimate packages that genuinely need install scripts (like node-gyp and imagemin) are automatically whitelisted.&lt;/p&gt;

&lt;p&gt;Layer 4 — Deep Tarball AST Scan&lt;br&gt;
Downloads the actual npm tarball for flagged packages and scans every JavaScript and TypeScript file inside it for 14 malware patterns: eval() with encoded payloads, new Function(), vm.runInNewContext(), child_process spawning, dynamic require() calls, Base64-decoded payloads, zero-width and Zalgo character obfuscation, network exfiltration patterns, self-publication via npm publish, and more. This catches threats that registry metadata alone cannot reveal.&lt;/p&gt;

&lt;p&gt;Layer 5 — Typosquatting Detection&lt;br&gt;
Detects three classes of package name attacks entirely offline with no network calls. First, classic typosquatting — misspelled names like recat instead of react — using Damerau-Levenshtein edit distance checked against 250+ top npm packages. Second, homoglyph attacks where Unicode look-alike characters (Cyrillic, Greek) replace Latin letters, making reаct look identical to react on screen. Third, AI hallucination squatting — package names that AI coding assistants commonly invent but that do not actually exist on npm, leaving a gap for attackers to register.&lt;/p&gt;

&lt;p&gt;Layer 6 — Dependency Confusion&lt;br&gt;
Identifies internal or private package names that have been planted on the public npm registry. Attackers register a name like @yourcompany/internal-utils on public npm with an inflated version number (e.g., 99.0.0). Because npm resolves the highest semver by default, the malicious public package silently wins over your private registry. This layer detects freshly published packages with inflated versions and near-zero download counts under scopes that should be private.&lt;/p&gt;

&lt;p&gt;Layer 7 — Registry Risk Heuristics&lt;br&gt;
Scores each package against a set of behavioral signals pulled from the npm registry. The risk score combines: package age (newly published packages score much higher), number of maintainers, weekly download velocity relative to package age, whether the publisher account matches the listed maintainers (a key account-takeover signal), whether the GitHub organization matches the npm publisher, and whether the unpacked package size is suspiciously large. A single flagged signal is informational; multiple signals together produce a High or Critical rating.&lt;/p&gt;

&lt;p&gt;Layer 8 — Lockfile Full-Tree Scan&lt;br&gt;
Expands the complete dependency tree from your lockfile and checks every transitive package — not just the ones listed in your package.json. Supports npm lockfile versions 1, 2, and 3, yarn.lock v1, pnpm-lock.yaml versions 5 through 9, and Bun lockfiles. It also validates that every resolved URL points to the standard npm registry, flagging packages silently pulled from alternative or attacker-controlled registries. The 2018 event-stream attack only worked because the malicious payload was three levels deep in the dependency tree — exactly what this layer is designed to catch.&lt;/p&gt;

&lt;p&gt;Layer 9 — Overrides / Resolutions CVE Poisoning&lt;br&gt;
Checks the overrides field (npm) and resolutions field (yarn) in your package.json. These fields are commonly used to force a safe version of a transitive dependency — but they can also accidentally or maliciously pin a vulnerable version that overrides a safe one. Every version pinned in these fields is checked against OSV.dev for known CVEs, exactly like direct dependencies.&lt;/p&gt;

&lt;p&gt;Layer 10 — Deprecated Packages&lt;br&gt;
Queries the npm registry for the official deprecation flag on each of your packages. Deprecated packages receive no further security patches — any vulnerability discovered after deprecation will remain permanently unpatched. The alert shows the deprecation message left by the maintainer and, where available, the recommended migration path or alternative package.&lt;/p&gt;

&lt;p&gt;Layer 11 — License Compliance&lt;br&gt;
Checks the SPDX license identifier of each package against a configurable deny-list. The default deny-list covers licenses that are incompatible with closed-source commercial software: GPL-2.0, GPL-3.0, AGPL-3.0, and SSPL-1.0. AGPL-3.0 in particular requires you to open-source your entire application if you distribute it or run it as a network service. The deny-list is fully customizable per team via a .nsgrc.json file in the project root.&lt;/p&gt;

&lt;p&gt;Layer 12 — Unmaintained Packages&lt;br&gt;
Flags packages that have not received a new publish in longer than a configurable threshold, defaulting to 12 months. Unmaintained packages are a common entry point for supply-chain attacks: threat actors acquire abandoned maintainer accounts through credential stuffing or social engineering, then push malicious updates to packages that still have millions of weekly downloads from years-old package.json files that nobody has touched. The age threshold is fully configurable via npmSafetyGuard.unmaintained.months.&lt;/p&gt;

&lt;p&gt;Layer 13 — Phantom Dependencies&lt;br&gt;
Scans your entire workspace source code — all .ts, .js, .tsx, and .jsx files — for import and require() statements referencing packages that are not declared in your package.json. These are called phantom dependencies. They appear to work locally because npm hoists packages into a flat node_modules structure, so a package required by one of your declared dependencies is incidentally available to your code. They break silently in CI pipelines, Docker builds, or whenever the transitive provider is updated or removed.&lt;/p&gt;

&lt;p&gt;Layer 14 — OSSF Scorecard&lt;br&gt;
Queries the Open Source Security Foundation Scorecard API for each package's GitHub repository and retrieves a security hygiene score from 0 to 10. The score is based on 18 automated checks including: whether branch protection is enforced, whether pull request reviews are required before merging, whether CI tests run on every commit, whether releases are cryptographically signed, and whether known vulnerabilities are tracked and patched. A low score means the project has weak practices and would be easy for an attacker to compromise. The API is free, requires no key, and results are cached for 24 hours.&lt;/p&gt;

&lt;p&gt;Layer 15 — Socket.dev Supply Chain Score&lt;br&gt;
Integrates with the Socket.dev API to retrieve deep behavioral supply-chain intelligence for each package. Socket analyzes packages at the AST level before you install them, flagging unexpected environment variable reads, network calls, shell spawning, obfuscated code, and author-change signals that indicate a possible account takeover. Requires a Socket.dev API key. Results are cached for 6 hours.&lt;/p&gt;

&lt;p&gt;Layer 16 — ReversingLabs Spectra Assure&lt;br&gt;
An optional premium integration with ReversingLabs, the same binary analysis platform used by enterprise security teams and government agencies. It provides malware classification, tampering detection (identifying when a published package has been modified after upload — a definitive sign of supply-chain compromise), CVE enumeration with fix availability, and C2 callback detection. A free community tier is available for individual developers.&lt;/p&gt;

&lt;p&gt;Layer 17 — npm Provenance Attestations&lt;br&gt;
Checks whether each package has a Sigstore-signed provenance attestation, a feature introduced in npm 9.5 in 2023. A provenance attestation cryptographically links the published package to the exact GitHub repository, commit SHA, and CI workflow run that built it. Without an attestation, there is no proof that the published tarball matches what was in the source code — it could have been built locally, tampered with, or published from a compromised machine.&lt;/p&gt;

&lt;p&gt;Layer 18 — Package Upgrade Diff Scanner&lt;br&gt;
When you bump a package version, this layer downloads both the old tarball and the new tarball, runs the full 14-pattern AST scan on each, and reports only the patterns that are newly present in the upgraded version. Patterns that existed in both versions are intentionally suppressed to eliminate noise. This means you see exactly what new capabilities or risks were introduced in the upgrade — eval() that was not there before, a new child_process call, an unexpected HTTP request — and nothing else.&lt;/p&gt;

&lt;p&gt;Layer 19 — Supply Chain Graph&lt;br&gt;
Renders an interactive force-directed graph of your entire dependency tree inside a VS Code webview. Every node is color-coded by its highest severity finding: red for Critical, orange for High, yellow for Medium, green for clean. Edges connecting risky packages are highlighted. You can pan, zoom, drag nodes, and search. A statistics overlay shows the total package count and a breakdown by risk level. The graph is truncated at 500 nodes, prioritizing the riskiest packages first.&lt;/p&gt;

&lt;p&gt;Layer 20 — MCP Server Config Scanner&lt;br&gt;
Scans Model Context Protocol configuration files across your workspace and home directory — including .cursor/mcp.json, .vscode/mcp.json, and ~/.claude/claude_desktop_config.json — for malicious or typosquatted MCP server packages. MCP servers run as persistent background processes inside AI coding sessions like Cursor and Claude Code. A malicious MCP server can intercept every AI query, read your entire codebase, and exfiltrate any API keys passed to it through the env configuration block.&lt;/p&gt;

&lt;p&gt;Layer 21 — AI Config Guard&lt;br&gt;
Scans up to three levels deep inside your node_modules directory for packages that contain code designed to target AI developer tool credentials. Using approximately 20 attack signatures, it detects attempts to read Claude Code settings, Cursor configuration, GitHub Copilot tokens, and 1Password CLI credentials, followed by exfiltration patterns like unexpected network calls carrying the stolen data. This layer addresses an emerging class of supply-chain attack specifically targeting the AI-assisted development workflow.&lt;/p&gt;

&lt;p&gt;Layer 22 — Exposed Secrets Scanner&lt;br&gt;
Scans your workspace files for accidentally exposed secrets in .env*, .npmrc, *.pem, *.key, credentials.json, and secrets.json files. Detects over a dozen secret types: AWS access keys, Stripe live keys, GitHub Personal Access Tokens, Slack tokens, Google API keys, SendGrid keys, npm auth tokens, private keys in PEM format, and generic API_KEY, SECRET, TOKEN, and PASSWORD patterns. Severity is escalated to Critical if the file is not listed in .gitignore, because the secret will be committed to your repository on the next git commit. Files like .env.example and .env.sample are never scanned.&lt;/p&gt;

&lt;p&gt;Layer 23 — Native Binary Detection&lt;br&gt;
Detects precompiled .node binary files inside npm package tarballs. Native Node.js Addons are compiled C or C++ shared libraries that are loaded directly into the Node.js process. They are completely opaque to JavaScript static analysis tools — none of the 14 AST scan patterns in Layer 4, none of the other JS-based layers, can inspect what a .node binary actually does at runtime. Many legitimate packages use native bindings for performance (image processing, database drivers, cryptography), so findings are flagged as High rather than Critical, with guidance to audit the source code on GitHub and consider whether a pure-JavaScript alternative exists.&lt;/p&gt;

&lt;p&gt;⭐ Outro&lt;br&gt;
NPM Safety Guard covers the entire threat surface of the npm ecosystem — from known malware databases and CVEs, through behavioral heuristics and tarball analysis, all the way to AI credential targeting and exposed secrets. Most layers require no API keys and work completely offline. Install it once and your entire team is protected on every project, in every file, on every save.&lt;/p&gt;

&lt;p&gt;Available free on the VS Code Marketplace (ext install jomynn.npm-safety-guard) and the JetBrains Marketplace. Open source under the MIT license. Built by SendWaveHub.&lt;/p&gt;

&lt;p&gt;🔧 INSTALL&lt;/p&gt;

&lt;p&gt;VS Code / Cursor / Windsufl:&lt;br&gt;
Search "NPM Safety Guard" in the Extensions panel, or run:&lt;br&gt;
ext install jomynn.npm-safety-guard&lt;/p&gt;

&lt;p&gt;JetBrains (WebStorm, IntelliJ IDEA, Rider…):&lt;br&gt;
Settings → Plugins → Marketplace → search "NPM Safety Guard"&lt;/p&gt;

&lt;p&gt;🔗 LINKS&lt;/p&gt;

&lt;p&gt;VS Code Marketplace → &lt;a href="https://marketplace.visualstudio.com/items?itemName=jomynn.npm-safety-guard" rel="noopener noreferrer"&gt;https://marketplace.visualstudio.com/items?itemName=jomynn.npm-safety-guard&lt;/a&gt;&lt;br&gt;
JetBrains Marketplace → &lt;a href="https://plugins.jetbrains.com/plugin/com.sendwavehub.npmsafetyguard" rel="noopener noreferrer"&gt;https://plugins.jetbrains.com/plugin/com.sendwavehub.npmsafetyguard&lt;/a&gt;&lt;br&gt;
GitHub (open source, MIT) → &lt;a href="https://github.com/jomynn/npm-safety-guard" rel="noopener noreferrer"&gt;https://github.com/jomynn/npm-safety-guard&lt;/a&gt;&lt;br&gt;
SendWaveHub → &lt;a href="https://sendwavehub.tech" rel="noopener noreferrer"&gt;https://sendwavehub.tech&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;💛 SUPPORT THE PROJECT&lt;/p&gt;

&lt;p&gt;⭐ Star the repo on GitHub — it helps more developers discover it&lt;br&gt;
☕ Ko-fi one-off tip → &lt;a href="https://ko-fi.com/sendwavehubtech" rel="noopener noreferrer"&gt;https://ko-fi.com/sendwavehubtech&lt;/a&gt;&lt;br&gt;
💖 GitHub Sponsors (recurring) → &lt;a href="https://github.com/sponsors/jomynn" rel="noopener noreferrer"&gt;https://github.com/sponsors/jomynn&lt;/a&gt;&lt;br&gt;
🏢 Corporate sponsorship → &lt;a href="https://sendwavehub.tech/contact" rel="noopener noreferrer"&gt;https://sendwavehub.tech/contact&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
      <category>tooling</category>
    </item>
    <item>
      <title>I built a free IDE extension to catch malicious npm packages before they wreck your project</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Thu, 18 Jun 2026 10:16:30 +0000</pubDate>
      <link>https://dev.to/jomynn/i-built-a-free-ide-extension-to-catch-malicious-npm-packages-before-they-wreck-your-project-24oe</link>
      <guid>https://dev.to/jomynn/i-built-a-free-ide-extension-to-catch-malicious-npm-packages-before-they-wreck-your-project-24oe</guid>
      <description>&lt;p&gt;Supply-chain attacks via npm are up year-over-year — packages like &lt;code&gt;event-stream&lt;/code&gt;, &lt;br&gt;
the Lazarus group drops, and AI-hallucinated typosquats keep landing in real codebases. &lt;br&gt;
I got tired of finding out &lt;em&gt;after&lt;/em&gt; the fact, so I built &lt;strong&gt;NPM Safety Guard&lt;/strong&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;It scans your &lt;code&gt;package.json&lt;/code&gt; and lockfiles right inside your editor — no separate CLI step.&lt;/p&gt;

&lt;p&gt;Here's what it currently catches across &lt;strong&gt;22 detection layers&lt;/strong&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Known malicious packages&lt;/strong&gt; — DPRK RAT drops, Lazarus-linked packages, event-stream clones&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;CVEs&lt;/strong&gt; — via OSV.dev, cached locally (free, no API key needed)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Typosquatting &amp;amp; homoglyph attacks&lt;/strong&gt; — catches &lt;code&gt;lodahs&lt;/code&gt;, &lt;code&gt;reàct&lt;/code&gt;, and AI-hallucinated package names&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Install script hooks&lt;/strong&gt; — flags &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; before you run them&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Deep tarball AST scan&lt;/strong&gt; — detects obfuscation, &lt;code&gt;eval&lt;/code&gt;, and payload patterns in the actual source&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dependency confusion&lt;/strong&gt; — scoped packages planted on public npm to hijack private installs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Exposed secrets&lt;/strong&gt; — API keys, tokens, private keys accidentally left in &lt;code&gt;.env&lt;/code&gt;, &lt;code&gt;.npmrc&lt;/code&gt;, &lt;code&gt;.pem&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;MCP server config scanner&lt;/strong&gt; — catches typosquatted or malicious MCP transport configs&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Supply chain graph&lt;/strong&gt; — interactive force-directed graph with risk overlay from your lockfile&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;OSSF Scorecard + Socket.dev score&lt;/strong&gt; — security hygiene at a glance&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Where to get it
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;VS Code / Cursor / Windsurf →&lt;/strong&gt; &lt;a href="https://marketplace.visualstudio.com/items?itemName=jomynn.npm-safety-guard" rel="noopener noreferrer"&gt;VS Code Marketplace&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;VS Codium / open-source IDEs →&lt;/strong&gt; &lt;a href="https://open-vsx.org/extension/jomynn/npm-safety-guard" rel="noopener noreferrer"&gt;Open VSX Registry&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebStorm / IntelliJ IDEA / all JetBrains IDEs →&lt;/strong&gt; &lt;a href="https://plugins.jetbrains.com/plugin/com.sendwavehub.npmsafetyguard" rel="noopener noreferrer"&gt;JetBrains Marketplace&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;All free. No account required for the core layers. MIT licensed on the VS Code side.&lt;/p&gt;

&lt;h2&gt;
  
  
  Under the hood
&lt;/h2&gt;

&lt;p&gt;The VS Code extension is TypeScript. The JetBrains plugin is Kotlin. They share the same &lt;br&gt;
detection signatures bundled at build time — no cloud dependency for the core scan.&lt;/p&gt;

&lt;p&gt;CVE lookups hit OSV.dev with a 24-hour local cache so you're not waiting on a network &lt;br&gt;
call every keystroke.&lt;/p&gt;




&lt;p&gt;Have you been burned by a supply-chain attack before? Or do you have a detection layer &lt;br&gt;
you wish existed? Drop it in the comments — I'm actively adding new signatures.&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>node</category>
      <category>security</category>
      <category>showdev</category>
    </item>
    <item>
      <title>How We Catch the Axios DPRK RAT — Directly in Your IDE</title>
      <dc:creator>jomynn</dc:creator>
      <pubDate>Fri, 22 May 2026 02:26:06 +0000</pubDate>
      <link>https://dev.to/jomynn/how-we-catch-the-axios-dprk-rat-directly-in-your-ide-449</link>
      <guid>https://dev.to/jomynn/how-we-catch-the-axios-dprk-rat-directly-in-your-ide-449</guid>
      <description>&lt;p&gt;&lt;em&gt;Published by SendWaveHub · NPM Safety Guard v1.12.0&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Attack
&lt;/h2&gt;

&lt;p&gt;In April 2025, security researchers discovered that &lt;strong&gt;axios 1.14.1&lt;/strong&gt; — a package with &lt;strong&gt;hundreds of millions of downloads&lt;/strong&gt; — had been briefly replaced on npm with a version containing a Remote Access Trojan (RAT) attributed to DPRK-linked threat actors (the Lazarus Group).&lt;/p&gt;

&lt;p&gt;The malicious version was live for a short window. Developers who ran &lt;code&gt;npm install&lt;/code&gt; during that window pulled down a backdoor that connected to attacker-controlled infrastructure.&lt;/p&gt;

&lt;p&gt;This isn't an edge case. It's the same pattern used in the &lt;strong&gt;event-stream&lt;/strong&gt; attack (2018), &lt;strong&gt;ua-parser-js&lt;/strong&gt; (2021), and dozens of others. Supply chain attacks now account for a significant share of real-world breaches — and they bypass most traditional security tools because &lt;strong&gt;the package itself is the attack vector&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem with Existing Tooling
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;npm audit&lt;/code&gt; only catches &lt;strong&gt;known CVEs&lt;/strong&gt; — it doesn't flag malicious payloads injected into legitimate packages. By the time a CVE is filed, the damage may already be done.&lt;/p&gt;

&lt;p&gt;OSV.dev, Snyk, and similar tools are excellent at CVE tracking, but they rely on a package being reported and catalogued. A supply chain attack that's live for 48 hours may never make it into those databases in time to protect you.&lt;/p&gt;




&lt;h2&gt;
  
  
  What NPM Safety Guard Does Differently
&lt;/h2&gt;

&lt;p&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=Sendwavehubtech.npm-safety-guard" rel="noopener noreferrer"&gt;NPM Safety Guard&lt;/a&gt; is a free VS Code extension that runs &lt;strong&gt;13 detection layers&lt;/strong&gt; directly in your editor, before &lt;code&gt;npm install&lt;/code&gt; even runs.&lt;/p&gt;

&lt;p&gt;For the Axios DPRK RAT specifically, it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Matches against a curated malicious-package database&lt;/strong&gt; — the bundled DB includes &lt;code&gt;axios@1.14.1&lt;/code&gt;, flagged as CRITICAL with the exact version and attack description&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Pulls a live remote feed&lt;/strong&gt; — updated independently of extension releases, so new attacks light up within hours of discovery&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Checks OSV.dev in real time&lt;/strong&gt; — CVEs filed against your installed versions are flagged inline&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Audits install hooks&lt;/strong&gt; — &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt;/&lt;code&gt;prepare&lt;/code&gt; scripts are flagged before they run, since they're the #1 supply-chain execution vector&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here's what it looks like when &lt;code&gt;axios@1.14.1&lt;/code&gt; appears in your &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;🔴 CRITICAL — DPRK RAT (Lazarus Group) — axios@1.14.1
This version was replaced with a Remote Access Trojan.
Safe version: 1.7.9
npm install axios@1.7.9
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The warning appears inline, in the Problems panel, and in a full Security Report — all before you've run a single &lt;code&gt;npm&lt;/code&gt; command.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Other 12 Detection Layers
&lt;/h2&gt;

&lt;p&gt;Catching known-malicious packages is the floor, not the ceiling. NPM Safety Guard also detects:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Layer&lt;/th&gt;
&lt;th&gt;What it catches&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Typosquatting&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;lодash&lt;/code&gt; (Cyrillic о), &lt;code&gt;expres&lt;/code&gt; — packages designed to look like legitimate ones&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Dependency confusion&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Scoped packages (&lt;code&gt;@company/pkg&lt;/code&gt;) that have been planted on public npm&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Overrides/resolutions poisoning&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Teams pin versions to fix a transitive CVE, accidentally pinning to a vulnerable version&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Phantom dependencies&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Packages your code imports but aren't in &lt;code&gt;package.json&lt;/code&gt; — fail silently in clean installs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Unmaintained packages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Last release &amp;gt;24 months ago → accumulating unpatched CVEs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Deprecated packages&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;npm-deprecated packages with auto-upgrade suggestions&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Install script audit&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Every &lt;code&gt;preinstall&lt;/code&gt;/&lt;code&gt;postinstall&lt;/code&gt; hook flagged with package context&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;OSSF Scorecard&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Security hygiene scores from the OpenSSF for each dependency&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Socket.dev integration&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Supply chain risk scores (requires token)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;License compliance&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;GPL/AGPL in commercial projects&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;ReversingLabs&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Real-time CVE + malware detection (requires free token)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Lockfile scan&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;Full resolved dependency tree, not just direct deps&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;




&lt;h2&gt;
  
  
  Install It Free
&lt;/h2&gt;

&lt;p&gt;The extension is free, open source (MIT), and available on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://marketplace.visualstudio.com/items?itemName=Sendwavehubtech.npm-safety-guard" rel="noopener noreferrer"&gt;VS Code Marketplace&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;
&lt;a href="https://open-vsx.org/" rel="noopener noreferrer"&gt;Open VSX&lt;/a&gt; (for VSCodium users)
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;ext &lt;span class="nb"&gt;install &lt;/span&gt;Sendwavehubtech.npm-safety-guard
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No API key required for the core features. The malware DB and OSV.dev integration work out of the box.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Matters Now
&lt;/h2&gt;

&lt;p&gt;The Axios attack was caught relatively quickly — but only because it targeted a high-profile package with many eyes on it. The next attack will target a smaller package, with fewer watchers, and stay live longer.&lt;/p&gt;

&lt;p&gt;Your best defense is catching the package &lt;strong&gt;before it's installed&lt;/strong&gt; — at the point where you're editing &lt;code&gt;package.json&lt;/code&gt; in your editor.&lt;/p&gt;

&lt;p&gt;That's exactly what NPM Safety Guard is built for.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Built by &lt;a href="https://sendwavehub.tech" rel="noopener noreferrer"&gt;SendWaveHub&lt;/a&gt;. If this saves your project, consider &lt;a href="https://github.com/sponsors/jomynn" rel="noopener noreferrer"&gt;sponsoring the work&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Tags:&lt;/strong&gt; &lt;code&gt;security&lt;/code&gt; &lt;code&gt;npm&lt;/code&gt; &lt;code&gt;javascript&lt;/code&gt; &lt;code&gt;devsecops&lt;/code&gt; &lt;code&gt;supplychain&lt;/code&gt;&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>javascript</category>
      <category>npm</category>
      <category>security</category>
    </item>
  </channel>
</rss>
