<?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: KirtashDev</title>
    <description>The latest articles on DEV Community by KirtashDev (@kirtashdev).</description>
    <link>https://dev.to/kirtashdev</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%2F4060608%2Fd7b0c1a0-e790-4941-81c5-f8755593a458.png</url>
      <title>DEV Community: KirtashDev</title>
      <link>https://dev.to/kirtashdev</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kirtashdev"/>
    <language>en</language>
    <item>
      <title>My npm supply-chain monitor was defeated by one line of JavaScript</title>
      <dc:creator>KirtashDev</dc:creator>
      <pubDate>Mon, 03 Aug 2026 12:24:33 +0000</pubDate>
      <link>https://dev.to/kirtashdev/my-npm-supply-chain-monitor-was-defeated-by-one-line-of-javascript-1hhm</link>
      <guid>https://dev.to/kirtashdev/my-npm-supply-chain-monitor-was-defeated-by-one-line-of-javascript-1hhm</guid>
      <description>&lt;p&gt;I maintain &lt;a href="https://github.com/KirtashDev/dephawk" rel="noopener noreferrer"&gt;dephawk&lt;/a&gt;, a runtime tripwire&lt;br&gt;
for npm dependencies. It patches the sensitive Node built-ins — &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;http&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;child_process&lt;/code&gt;, &lt;code&gt;process.env&lt;/code&gt; and friends — captures a stack trace on every&lt;br&gt;
call that touches something it cares about, walks that stack to find the first&lt;br&gt;
&lt;code&gt;node_modules/&amp;lt;package&amp;gt;&lt;/code&gt; frame, and checks that package against your policy. In&lt;br&gt;
enforce mode it throws instead of letting the call through.&lt;/p&gt;

&lt;p&gt;The pitch is a screenshot: a package reads &lt;code&gt;~/.ssh/id_rsa&lt;/code&gt;, and dephawk names&lt;br&gt;
it.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FKirtashDev%2Fdephawk%2Fmain%2Fassets%2Fdemo.gif" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fraw.githubusercontent.com%2FKirtashDev%2Fdephawk%2Fmain%2Fassets%2Fdemo.gif" alt="dephawk catching a dependency listing ~/.ssh, reading a crypto wallet key and an npm token, shelling out with a credential, and phoning home — then blocking all of it in enforce mode" width="800" height="465"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Last week I stopped reading my own code and started attacking it. It lost to&lt;br&gt;
this:&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;setTimeout&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="nx"&gt;readFileSync&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="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/home/you/.ssh/id_rsa&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is the entire bypass. No &lt;code&gt;Error.prepareStackTrace&lt;/code&gt; tricks, no native addon,&lt;br&gt;
no obfuscation. A dependency that runs that line reads your private key, and&lt;br&gt;
dephawk — in enforce mode, with a deny-everything policy — reports it as &lt;strong&gt;your&lt;br&gt;
own code&lt;/strong&gt; and allows it.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why it works
&lt;/h2&gt;

&lt;p&gt;Look at what is &lt;em&gt;missing&lt;/em&gt;. &lt;code&gt;setTimeout&lt;/code&gt; is handed &lt;code&gt;fs.readFileSync&lt;/code&gt; itself, not&lt;br&gt;
a closure that calls it. So when the timer fires, the stack is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="go"&gt;at readFileSync (node:fs:…)
at listOnTimeout (node:internal/timers:581:17)
at process.processTimers (node:internal/timers:519:7)
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;There is no frame belonging to the package, because no function of the package&lt;br&gt;
is running. Its involvement ended the moment it scheduled the call.&lt;/p&gt;

&lt;p&gt;If you write the same thing as a closure, dephawk catches it fine:&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;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;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;secret&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="c1"&gt;// caught — the arrow is yours&lt;/span&gt;
&lt;span class="nf"&gt;setTimeout&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="nx"&gt;readFileSync&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="nx"&gt;secret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt; &lt;span class="c1"&gt;//     — not caught&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The difference is whether a function &lt;em&gt;you wrote&lt;/em&gt; appears on the stack. V8 keeps&lt;br&gt;
the source location of the closure, so the first form still points at your file.&lt;br&gt;
The second form hands over a built-in, and built-ins have no &lt;code&gt;node_modules&lt;/code&gt; path.&lt;/p&gt;
&lt;h2&gt;
  
  
  The actual bug: a sentinel that meant two things
&lt;/h2&gt;

&lt;p&gt;Losing the frame should have cost the attacker a name in a report. Instead it&lt;br&gt;
bought them a free pass. Here is why.&lt;/p&gt;

&lt;p&gt;The attributor returns &lt;code&gt;null&lt;/code&gt; when it finds no dependency frame:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;attribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;rawStack&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Attribution&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;attributed&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// …walk frames, set `attributed` on the first node_modules/&amp;lt;pkg&amp;gt; frame…&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;package&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;attributed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;frames&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;And the policy engine did this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;evaluate&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;CapabilityRequest&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="nx"&gt;Verdict&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;sensitive&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;detectSensitive&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="k"&gt;if &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="kr"&gt;package&lt;/span&gt; &lt;span class="o"&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="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;allowed&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="nx"&gt;sensitive&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;  &lt;span class="c1"&gt;// ← here&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;pkg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;packages&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="kr"&gt;package&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;policy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;default&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;evaluateCapability&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;pkg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;sensitive&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;Both of those are individually reasonable.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;null&lt;/code&gt; for "no package frame found" is the obvious return value. And allowing&lt;br&gt;
&lt;code&gt;null&lt;/code&gt; unconditionally comes from a real product decision that is right: &lt;em&gt;your&lt;br&gt;
own application code is never flagged — dephawk watches dependencies, not you.&lt;/em&gt;&lt;br&gt;
A security tool that screamed every time &lt;strong&gt;you&lt;/strong&gt; read a &lt;code&gt;.env&lt;/code&gt; file would be&lt;br&gt;
uninstalled by lunchtime.&lt;/p&gt;

&lt;p&gt;The bug lives in the seam between them. &lt;code&gt;null&lt;/code&gt; was carrying two meanings:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;"this is the application"&lt;/strong&gt; — trust it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;"attribution failed"&lt;/strong&gt; — no idea who this was&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;and the engine picked the friendlier reading for both. Every capability was&lt;br&gt;
affected — filesystem, network, spawn, environment variables — and&lt;br&gt;
&lt;code&gt;.then(fs.readFileSync)&lt;/code&gt; gets you the same result through promises.&lt;/p&gt;
&lt;h2&gt;
  
  
  The part that actually stings
&lt;/h2&gt;

&lt;p&gt;None of this was hidden. My own architecture decision record, written when&lt;br&gt;
attribution was designed, lists the ways out:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;Attribution is &lt;strong&gt;high-signal, not tamper-proof&lt;/strong&gt;. A determined attacker can:&lt;br&gt;
[…] defer work to a detached callback/timer so the originating frame is gone;&lt;/p&gt;

&lt;p&gt;Async gaps can also drop the originating frame, yielding &lt;code&gt;package: null&lt;/code&gt;&lt;br&gt;
(attributed to "your code"). We accept these limits and state them plainly in&lt;br&gt;
the README.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;I had written down the behaviour. I had even written down the &lt;em&gt;consequence&lt;/em&gt; —&lt;br&gt;
"attributed to your code" — and then stopped one sentence short of asking what&lt;br&gt;
the policy engine does with that.&lt;/p&gt;

&lt;p&gt;Writing the limitation down had made it feel handled.&lt;/p&gt;

&lt;p&gt;That is the bit worth stealing from this post. An accepted limitation is a&lt;br&gt;
decision, and decisions age. This one was genuinely fine on the day it was&lt;br&gt;
written, and stopped being fine the moment &lt;code&gt;package: null&lt;/code&gt; became load-bearing&lt;br&gt;
for a &lt;em&gt;trust&lt;/em&gt; decision. Nobody re-read it, because it was already in the docs,&lt;br&gt;
and things in the docs feel settled.&lt;/p&gt;
&lt;h2&gt;
  
  
  The fix
&lt;/h2&gt;

&lt;p&gt;Two changes, and only the first one is a security fix.&lt;/p&gt;
&lt;h3&gt;
  
  
  First: attribution now has three answers, not two
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;type&lt;/span&gt; &lt;span class="nx"&gt;Origin&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dependency&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;application&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;unknown&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;A frame naming a real source file means the application. Runtime internals,&lt;br&gt;
native and anonymous frames only means &lt;em&gt;nobody could be identified&lt;/em&gt;. Only&lt;br&gt;
&lt;code&gt;application&lt;/code&gt; keeps the unconditional allow — &lt;code&gt;unknown&lt;/code&gt; gets evaluated against&lt;br&gt;
the default policy bucket, exactly like an unlisted package.&lt;/p&gt;

&lt;p&gt;That alone closes it. With any deny-by-default config, the laundered read is&lt;br&gt;
denied. Losing your name now costs you the benefit of the doubt instead of&lt;br&gt;
granting it.&lt;/p&gt;
&lt;h3&gt;
  
  
  Second: making the report useful again
&lt;/h3&gt;

&lt;p&gt;Denied-but-anonymous is safe and useless — the report says &lt;code&gt;(unattributed)&lt;/code&gt; and&lt;br&gt;
you still don't know which dependency to remove. So a scheduler interceptor&lt;br&gt;
patches &lt;code&gt;setTimeout&lt;/code&gt;/&lt;code&gt;setInterval&lt;/code&gt;/&lt;code&gt;setImmediate&lt;/code&gt;, &lt;code&gt;queueMicrotask&lt;/code&gt;,&lt;br&gt;
&lt;code&gt;process.nextTick&lt;/code&gt; and &lt;code&gt;Promise.prototype.then&lt;/code&gt;. When the callback handed to one&lt;br&gt;
of them &lt;strong&gt;is an intercepted built-in&lt;/strong&gt;, it captures the stack at the scheduling&lt;br&gt;
site — where the culprit is still plainly visible — into an &lt;code&gt;AsyncLocalStorage&lt;/code&gt;&lt;br&gt;
context that rides along with the callback.&lt;/p&gt;

&lt;p&gt;The cost, measured rather than guessed: stacks are only captured for that&lt;br&gt;
specific shape, so ordinary scheduling pays one &lt;code&gt;WeakSet&lt;/code&gt; lookup per callback&lt;br&gt;
argument. On Node 22 that is &lt;strong&gt;+22 ns per explicit &lt;code&gt;.then()&lt;/code&gt;&lt;/strong&gt;. And &lt;code&gt;await&lt;/code&gt; on a&lt;br&gt;
native promise doesn't route through &lt;code&gt;Promise.prototype.then&lt;/code&gt; at all — I checked:&lt;br&gt;
0 interceptions across 10,000 awaits — so async/await-heavy code pays nothing&lt;br&gt;
there.&lt;/p&gt;
&lt;h2&gt;
  
  
  Two more, once I was looking properly
&lt;/h2&gt;

&lt;p&gt;Finding one bypass by attacking instead of reading strongly suggested doing more&lt;br&gt;
of it. Two more fell out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The audit log was writable by the thing being audited.&lt;/strong&gt; &lt;code&gt;dephawk guard&lt;/code&gt;&lt;br&gt;
aggregates every process an install spawns into one shared JSONL file, and the&lt;br&gt;
path travels in an env var so it can reach npm's lifecycle scripts. A malicious&lt;br&gt;
&lt;code&gt;postinstall&lt;/code&gt; needed one call:&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;fs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;truncateSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;DEPHAWK_SINK&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;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reproduced: a script that read an SSH key, followed by one that wiped the sink,&lt;br&gt;
produced a clean report.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Monitoring spread by inheritance, so it could be declined.&lt;/strong&gt; dephawk reaches a&lt;br&gt;
process tree via &lt;code&gt;NODE_OPTIONS&lt;/code&gt;. Anything you can inherit, you can decline to&lt;br&gt;
pass on:&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;clean&lt;/span&gt; &lt;span class="o"&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;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt; &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;NODE_OPTIONS&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nf"&gt;spawnSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;execPath&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;payload.js&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;clean&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The spawn was recorded. Everything the child did afterwards was invisible.&lt;/p&gt;

&lt;p&gt;The fix there is interesting because it &lt;em&gt;isn't&lt;/em&gt; to block it: building a clean&lt;br&gt;
environment for a child is completely legitimate — reproducible builds do&lt;br&gt;
exactly this — and nothing distinguishes it from an escape attempt. So dephawk&lt;br&gt;
puts the missing pieces back into every child it lets through, and notes it in&lt;br&gt;
the report.&lt;/p&gt;

&lt;h2&gt;
  
  
  What I'd take away from this
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;A sentinel that means two things will eventually mean the wrong one.&lt;/strong&gt; &lt;code&gt;null&lt;/code&gt;&lt;br&gt;
for "the application" and &lt;code&gt;null&lt;/code&gt; for "I don't know" were the same value right up&lt;br&gt;
until something important depended on telling them apart. The fix wasn't&lt;br&gt;
cleverness — it was a third case.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fail closed on ignorance.&lt;/strong&gt; "I couldn't figure out who did this" should cost&lt;br&gt;
trust, not grant it. When your system can't answer a question it needs answered,&lt;br&gt;
the safe default is the answer that assumes the worst, not the one that's&lt;br&gt;
quietest.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Re-read your own accepted limitations.&lt;/strong&gt; This was documented as acceptable in&lt;br&gt;
the file whose entire job is recording that kind of decision. Nothing prompts you&lt;br&gt;
to revisit something that already looks settled.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Attack it, don't audit it.&lt;/strong&gt; All three of these came from trying to defeat the&lt;br&gt;
tool. None came from reading the code looking for bugs — the code was well&lt;br&gt;
covered by tests, and all three bypasses passed every single one. Tests written&lt;br&gt;
from the design will confirm the design.&lt;/p&gt;




&lt;p&gt;Full write-up with the reasoning and the architecture decision records:&lt;br&gt;
&lt;a href="https://github.com/KirtashDev/dephawk/blob/main/docs/attacking-dephawk.md" rel="noopener noreferrer"&gt;Three ways out of a runtime supply-chain monitor&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;And none of this makes dephawk a sandbox — attribution still rests on stack&lt;br&gt;
traces, native addons run outside the JS surface entirely, and &lt;code&gt;eval()&lt;/code&gt; can't be&lt;br&gt;
patched. It's a high-signal tripwire, not a boundary. What changed is that the&lt;br&gt;
cheapest way out, the one that needed no privileged position and read like&lt;br&gt;
perfectly ordinary async code, is closed.&lt;/p&gt;

</description>
      <category>node</category>
      <category>security</category>
      <category>javascript</category>
      <category>opensource</category>
    </item>
  </channel>
</rss>
