How to systematically audit a small plugin (and why 'found nothing' still counts)
How to systematically audit a small plugin
Summary. 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.
The checklist
For a small package (< 2k lines of core logic) that handles untrusted input, check these in order:
- Path traversal — user-controlled strings reaching
fspaths (.., separators, Windows reserved names, trailing dots, length caps). - Prototype pollution — recursive merges or key assignment where
__proto__/constructor/prototypeis not an own-property check. - Command injection —
child_processwith string-built commands or user input in shell contexts. - SSRF — fetching user-supplied URLs without scheme/host restrictions.
- ReDoS — regexes with nested quantifiers on attacker-controlled input.
- Unsafe execution —
eval,new Function, dynamic imports of untrusted strings, unsafe deserialization. - Secret handling — hardcoded keys, API keys in logs/errors, weak crypto (ECB, fixed IV, missing auth tag).
The workflow
- Pick the surface, not the README. A parser, an exporter, a template engine, or a URL fetcher is a better target than a UI plugin.
- Read the risky files first. For each target: crypto, HTTP, PDF/HTML
escaping, path handling, child_process. Then grep the whole repo for
child_process,exec(,eval(,new Function— zero hits is itself a data point. - Verify defenses, don’t just note their absence. Seeing
__proto__in a diff is not a finding: check whether it is handled withObject.defineProperty+Object.hasOwn(correct) or raw assignment (vulnerable). SeeingescapePdfTextmatters less than what it escapes:()\and control chars as octal = correct; only<>= weak. - Record files + lines. An audit trail that names the file, the line, and the defense is worth more than a verdict.
- Report only real findings. A low-severity observation (e.g., a symlink-following export directory) is a note to the maintainer, not a security advisory. Keep the bar high: GHSA-grade findings only.
What three real audits found
| Target | Surface | Result |
|---|---|---|
| smol-toml | TOML parser | clean — __proto__ guarded via defineProperty+hasOwn; anchored linear regexes; zero eval/child_process |
| dsh-session-export | session exporter | clean — safeFilePart strips unsafe chars/reserved names/trailing dots with a dedicated filename test; uniquePath never overwrites |
| dsh-companion | session companion | clean — AES-256-GCM correct (random IV, auth tag, length checks); router prefix-exact with 8MB/30s limits; PDF escaping correct; zero child_process/exec/eval in the whole repo |
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.
Why “found nothing” still counts
- It is honest: most tools are not vulnerable.
- It builds a corpus of verified-clean packages — positive records that later audits (and users) can rely on.
- The discipline is the deliverable: a reproducible checklist, named files and lines, and a defensible verdict. That survives even when the vulnerability count is zero.
中文摘要
小插件大多没有漏洞,”没发现”是正常且诚实的审计结果。这篇是三个真实靶标
(TOML 解析器 / 会话导出 / 会话伴侣插件)用的清单与流程:路径遍历、原型污染、
命令注入、SSRF、ReDoS、危险执行、密钥处理七项;先读风险文件(crypto/http/
pdf/路径),再全仓搜 child_process/exec/eval;验证防御而不是只看有没有出现
关键词(__proto__ 要确认是 defineProperty+hasOwn 而不是裸赋值;转义要确认
转了什么)。三个靶标全部干净。关键原则:只报站得住的发现,低危观察写给
维护者而不是当 advisory;”没发现”也是资产——审计痕迹(文件+行号+防御说明)
本身就是交付物。