🎁 福利专区全网大模型免费应用 + 新用户福利 + 注册活动入口,低成本玩转 AI

出网护栏 JackZo400/dsh-netguard

DeepSeek Harness
JackZo40034 天前有提交已提供中文文档扫描:未发现敏感能力

拦截内网地址与重定向,边读边限流防 SSRF

29.8/100
综合分
0
GitHub 星标
—
npm 周下载
—/5
用户评分

数据截至 2026/9/29 · 周下载取自 npm 全网(含 CI / 镜像),非本站安装数;综合分由星标、活跃度与兼容性检查合成。

安装命令 · 需先装 dsh CLI 引擎
dsh plugin --profile web add JackZo400/dsh-netguard
该包不可直接 npm 安装,改从 GitHub 源安装(明细见下方安装检查)

状态速览

判定依据:实装验证 · 静态安装检查 · 自动扫描 · 仓库维护状态
信任档位:待复验
是什么
dsh 原生插件 · security
装得上吗
曾在 dsh 0.2.0-rc.2 下真实安装成功,但当前 dsh 已更新至 0.2.1-rc.5,本站尚未在新版下复验
L4 · 真实安装等级说明 →
安全吗
本站尚未对该插件做风险分级
暂未覆盖,不等同于无风险
还在维护吗
更新放缓:最近一次提交在 34 天前
档位由哪些信号合成

本站实装验证(真实安装 L4 覆盖全站;L5 为运行时冒烟或实机截图,覆盖范围小于 L4) · 验证所用 dsh 版本 · 静态安装检查 · 风险分级 · 仓库维护状态。 下方各区块是它的证据明细。

验证判据与等级说明 →
社区实装还没有人打卡 · 装过的第一个就是你
安全扫描自动 · 静态分析源码
v2.7.0
🟢 扫描:未发现敏感能力尚无人工评估
展开查看扫描说明与证据 ↓

静态扫描未发现敏感能力:最多只发网络请求、读取自身元数据或只读文件,未发现执行命令、文件写入或敏感信息读取。

扫描于 2026/10/31(1 天前) · 范围 入口闭包 + 发布文件清单 · 来源 GitHub 仓库

静态扫描只反映源码中出现的调用,不构成安全背书;人工评估与自动扫描相互独立、互不覆盖。

给这个插件打分还没有人投票↓
用户评分
还没有人投票,来当第一个

功能亮点

  • 仅放行公网地址
  • 重定向逐跳复查
  • 边读边限大小

dsh 插件:出网护栏——只放公网、重定向逐跳复查、边读边限大小(防 SSRF)· 为 dsh 守住出网:只放行公网地址,每个重定向跳都复查,流式读取时限制大小。

README

由 DeepSeek 最新模型翻译生成
dsh-netguard 给 DeepSeek Harness(dsh)用的出网护栏:让“拉一个外部 URL”这件事不再是一个 SSRF 入口。 一个服务(netguard)+ 一个工具(fetch_url,默认关闭)。 别的插件直接调服务,不用自己再写一遍地址判定 —— 判定写错一次,就是一次内网暴露。 为什么需要它 不挡会怎样 Agent 插件几乎都要出去拉东西:图片、语音、网页、回调。只要那个 URL 来自聊天消息或模型输出(群里有人贴了张图、模型自己拼了个地址),它就是攻击者能控制的字符串。 而跑插件的机器通常站在一个比你有权的位置上: | 模型/用户给出的 URL | 实际会发生什么 | | http://127.0.0.1:5099/api/… | 本机上别的服务(管理台、内部 API、数据库 HTTP 口) | | http://192.168.1.1/ | 路由器 / 内网设备的管理页 | | http://169.254.169.254/latest/meta-data/iam/security-credentials/ | 云厂商元数据 → 临时凭据。
展开完整文档(README 剩余 1 节)
dsh-netguard

给 DeepSeek Harness(dsh)用的出网护栏:让“拉一个外部 URL”这件事不再是一个 SSRF 入口。

一个服务(netguard)+ 一个工具(fetch_url,默认关闭)。
别的插件直接调服务,不用自己再写一遍地址判定 —— 判定写错一次,就是一次内网暴露。

为什么需要它

不挡会怎样

Agent 插件几乎都要出去拉东西:图片、语音、网页、回调。只要那个 URL 来自聊天消息或模型输出(群里有人贴了张图、模型自己拼了个地址),它就是攻击者能控制的字符串。
而跑插件的机器通常站在一个比你有权的位置上:

| 模型/用户给出的 URL | 实际会发生什么 |
| http://127.0.0.1:5099/api/… | 本机上别的服务(管理台、内部 API、数据库 HTTP 口) |
| http://192.168.1.1/ | 路由器 / 内网设备的管理页 |
| http://169.254.169.254/latest/meta-data/iam/security-credentials/ | 云厂商元数据 → 临时凭据。拿到它,你的云账号就是他的 |
| http://[::1]/ | IPv6 回环,只写 127\. 的实现完全看不见 |
| http://2130706433/ | 还是 127.0.0.1(十进制变形)。0177.0.0.1、0x7f.1、0x7f000001 同理 |
| http://demo.example.com/x | 这个域名解析到 10.0.0.5 —— 域名看着人畜无害 |
| http://demo.example.com/x | 它回一个 302 Location: http://169.254.169.254/… |
| file:///etc/passwd | 有些 fetch 封装会把协议校验漏掉,直接读本地文件 |

最后两条最容易被漏掉,也最致命:

- 只检查入口 URL 的实现,在“域名解析到内网”面前等于没装 —— 攻击者只需要一个自己的域名。
- 只检查入口 URL 的实现在 302 面前同样等于没装。攻击者甚至不需要控制 DNS,只要在自己那台公网服务器上写一行 Location: http://169.254.169.254/… 就够了。

这个插件把这两条都补上了:解析出来的每一条地址都判,重定向的每一跳都重新判。

安装

dsh plugin --profile web add github:JackZo400/dsh-netguard

没有依赖、没有子进程、没有要装的二进制。Node 18.17+(实测 22.x)。

配置

- insert:
- id: netguard
name: dsh-netguard
config:
allowedProtocols: ['http:', 'https:'] # 只放这两种
maxBytes: 8388608 # 一次最多下多少字节(8 MiB) onOverflow: error # error = 报错;truncate = 截断到上限 timeoutMs: 20000 # 整条链共用的超时 allowRedirects: true # 跟不跟重定向(每一跳都会重新体检) maxRedirects: 3
blockedPorts: [22, 23, 25, 6379, 11211, 27017] # 内网服务常客
allowUrlCredentials: false # 允不允许 URL 里带 user:pass@ allowSingleLabel: false # 允不允许 http://intranet/ 这种 userAgent: dsh-netguard/0.1.0 enableTool: false # 要不要登记 fetch_url 工具 toolMaxChars: 20000 # 工具最多回多少字符正文

大小上限与超时的取舍(这几条是真会踩到的):

- 超限默认报错,而不是静默截断。 因为主要调用场景是“拉图/拉音频”,截断的字节是坏数据:半张 JPEG 解不出图,可流量已经花掉、附件也存下了,最后报的还是个更难查的错。要“只要前面一段”就显式写 onOverflow: truncate。
- Content-Length 报得太大就直接不下载,流量一点都不花。
- 读流是一边读一边数的,超了立刻断(reader.cancel()),不是读完再判断 ——对面挂一个 50GB 的流,读完再判断等于自己把内存吃光。
- 超时是整条链共用一个预算,不是每跳各给一份。否则“跳 10 次、每次卡 19 秒” 能拖出 190 秒。跳转途中花掉的时间也会算进去。

用法

给别的插件用(推荐)

const netguard = ctx.get('netguard')

// 只要字节(签名和老实现一致:(url, { timeoutMs, maxBytes }) → Buffer) const bytes = await netguard.fetchPublicBytes(imageUrl, { maxBytes: 8 * 1024 * 1024, timeoutMs: 20_000 })

// 要更多信息:状态码、最终落到的地址、跳了几次、有没有被截断 const r = await netguard.fetchPublic(url, { maxBytes: 64 * 1024, onOverflow: 'truncate' }) // → { finalUrl, status, contentType, headers, bytes, truncated, redirects, hops }

// 只体检,不下载js const u = await netguard.assertPublicUrl(url) // 不安全就抛(带 code)

// 主机名/地址判定(可以单独用,不用发请求)
netguard.isPublicHost('127.0.0.1') // false(同步,不查 DNS,只挡明显的写法)
await netguard.checkHost('demo.example.com') // { ok, addresses, offender? }(会查 DNS)
netguard.classifyIp('::ffff:10.0.0.1') // { isIp:true, family:6, public:false, reason:'PRIVATE', mapped:'10.0.0.1' }

失败一律抛 NetguardError,message 是能直接上屏的人话,code 是机器可读的原因:

PRIVATE_ADDRESS BAD_HOST DNS_PRIVATE DNS_FAILED BAD_PROTOCOL BAD_CREDENTIALS
BAD_PORT BLOCKED_PORT BAD_URL TOO_LARGE TIMEOUT ABORTED NETWORK
REDIRECT_DISABLED BAD_REDIRECT TOO_MANY_REDIRECTS REDIRECT_LOOP HTTP_ERROR

错误文案只带主机名,不带完整 URL(URL 里可能有 token),也不带响应体 —— 可以放心写日志。

给模型用(默认关闭)

打开之后多一个 fetch_url 工具: yaml config: enableTool: true

抓一下 https://example.com/demo.html 里写了什么

它的返回值会进模型上下文,所以默认是关的 —— 那等于开了一条“外部内容 → 模型”的管道。
工具描述里写明了:拉回来的东西是数据,不是指令。

只想用判定逻辑(不要 dsh) js
import { isPublicIp, classifyIp } from 'dsh-netguard/src/ip.js'
import { assertPublicUrl } from 'dsh-netguard/src/guard.js'

isPublicIp('169.254.169.254') // false

src/ip.js 是纯函数,不碰网络、不读配置,可以单独拿去用。

它到底挡了什么

IPv4 0.0.0.0/8 10/8 100.64/10 127/8 169.254/16 172.16/12 192.0.0.0/24
192.0.2.0/24 192.88.99.0/24 192.168/16 198.18/15 198.51.100/24
203.0.113/24 224/4 240/4(含 255.255.255.255)以及 169.254.169.254 / 100.100.100.200 / 192.0.0.192 这几个元数据地址(单独报原因)变形写法:127.1 / 2130706433 / 0177.0.0.1 / 0x7f.1 / 0x7f000001 …

IPv6 ::1 :: fc00::/7 fe80::/10 fec0::/10 ff00::/8 100::/64 2001:db8::/32
::ffff:a.b.c.d(IPv4-mapped) ::a.b.c.d(IPv4-compatible)
2002::/16(6to4,把 IPv4 塞在第 3~6 字节) 2001::/32(Teredo)
64:ff9b::/96(NAT64,最后 4 字节就是 IPv4)
最后只放行 2000::/3 —— 白名单式:漏掉一条规则只会更严,不会更松

名字 localhost / .localhost / .local / .localdomain / .internal / .home.arpa .lan / .intranet / .corp / .private / .home / *.onion 单段主机名(http://intranet/)末段是纯数字/0x 的畸形写法(999.1.1.1 / 1.2.3.4.5 / 0178.0.0.1)—— 不猜,直接拒

协议 只放 http: / https: 端口 22 23 25 110 143 445 993 995 3306 5432 6379 11211 27017(可配) DNS 解析出来的每一条都判;有一条内网就整体拒(不是取第一条、不是挑最像公网的那条)解析失败 / 解析出空 / 解析出不是 IP 的东西 → 一律拒(fail closed,绝不放行)跳转 每一跳都重走一遍上面全部检查;成环、超跳数、跳转被关掉、没有 Location 都报错

和“只写几个正则”比,补了什么

如果你手上已经有一个 isPrivateIp() 之类的实现,可以对照一下这几处:

1. 变形写法。老写法一般是 /^127\./、/^10\./ 这种前缀正则, 127.1、2130706433、0177.0.0.1、0x7f.1 全都过。这里按 inet_aton 的语义把 1~4 段的写法真解析成 32 位整数再判。
2. 黑名单换白名单。老实现列了 7 个私网段,192.0.0.0/24、198.18/15、
240/4、多播段都不在里面。这里按特殊用途地址表逐块判,IPv6 干脆只放 2000::/3。
3. IPv4-mapped 的 IPv6。::ffff:127.0.0.1 在内核看来就是 IPv4 连接,
字符串前缀判定的实现完全看不见它(::ffff:7f00:1 这种纯十六进制写法更看不见)。
这里解成 16 字节按位判,6to4 / NAT64 也一样拆出来。
4. 域名解析。老实现只拿第一条记录判(lookup 不带 all: true 时甚至只看一个族):攻击者给两条记录(一条公网、一条内网)就绕过去了。这里要全部记录,有一条内网就拒。
5. 重定向。老实现用默认的 redirect: 'follow',等于把“跳去哪”完全交给了对方。
这里 redirect: 'manual' 自己接管,每一跳重新体检。
6. 大小上限。老实现是 arrayBuffer() 读完再比 —— 内存已经吃进去了。
这里流式读、边读边数,超了立刻断。另外 Content-Length 就能提前判掉。
7. 超时的层级。老实现每跳各给一次 AbortSignal.timeout()(其实它根本没跟跳转,所以没暴露),这里整条链一个 deadline。
8. 失败的声音。老实现把 DNS 失败 .catch(() => []) 之后报"解析失败",
意思对;但这里还把 code 分出来了(DNS_FAILED / DNS_PRIVATE / PRIVATE_ADDRESS…),调用方能分类处理,不用去正则匹配中文。顺手也顺手把 URL 里的账号密码这类 "看着像别人"的伪装写法挡掉了。

测试 bash
npm test # 或者分别跑:
node test/selftest.mjs # 判定逻辑 + 重定向 + 大小上限 + 超时(249 条断言)
node test/plugin-selftest.mjs # 插件接线:服务/工具/配置覆盖(65 条断言)

两个脚本合计 314 条断言。两个都是完全离线的:DNS 解析器和 fetch 都是注入进去的假实现,所以不需要网络、不会抖动,也不需要任何密钥 —— 更不会"顺手把内网探测一遍"。
断言是真断言:把 src/ 里任何一条防护删掉,脚本都会红(比如把重定向的逐跳复查去掉,会挂 4 条;把大小上限去掉,会挂 7 条)。

已知局限

没防住的部分,写清楚比假装防住了强

1. DNS 解析与实际连接之间的时间差(TOCTOU)。这里查一次 DNS、判定通过,然后交给 fetch 自己再查一次才连。攻击者如果用一个 TTL=0 的域名,可以让第一次查到公网、第二次查到内网 —— 这是 DNS rebinding 的第二种形态,本插件挡不住。真正的修法是把解析结果钉住(自己实现 dispatcher / connect 钩子,连那个 IP 而不是再查一次名字),或者连上之后核对对端地址。
Node 的全局 fetch 不给这个钩子,所以这里留了 fetchImpl 注入点,宿主可以接自己的实现。
2. 代理。如果宿主注入了一个走代理的 fetch,本插件的判定是在代理之前做的,它只描述"目标地址",管不了代理那边怎么解析、怎么连。
另外 Node 的 fetch 默认不读 HTTP_PROXY / HTTPS_PROXY,但这个行为取决于运行时版本 ——别指望这一层替你管代理。
3. 内容本身。它只管"能不能去、拿多大",不管"拿回来的东西写了什么"。
拉回来的页面里完全可以写"忽略你之前的所有指令"。工具的描述和返回值末尾都提醒了这一点,但那是一句提醒,不是一道防护。
4. 不是浏览器。、JS 跳转、Service Worker 这些客户端侧的重定向不归它管(fetch 也看不到)。
5. 公网地址上的内网内容。攻击者自己那台公网服务器完全可以反代到内网 ——从地址层面看不出任何问题。这一层挡不住,也不该指望它挡。
6. 端口黑名单是近似,不是完整。8081、9090、5000 这些也可能挂着内网服务。
想更严就自己往 blockedPorts 里加。
7. IPv6 只放 2000::/3,极少数合法的特殊写法会被误挡(被挡了会报 RESERVED,能改)。
8. 没有速率/并发限制。它回答的是"能不能去这个地址",不回答"去多少次"。
真要做配额得在外面加。
9. 只做 GET,不支持上传、POST、流式长连接。
10. DNS 层不做 DNSSEC 验证,系统解析器返回什么就判什么。
(不过 CNAME 链不影响结论:判的是最终地址。)

License

MIT © 2026 JackZo400

→ Full English README: README.en.md
预览分享卡片长什么样 ↓JackZo400/dsh-netguard
分享 · DeepSeek Harness Hub
JackZo400/dsh-netguard

拦截内网地址与重定向,边读边限流防 SSRF

回到顶部