dsh-netguard:出网护栏(SSRF 防护)

DeepSeek Harness(DSH)的插件开发中,经常涉及从外部拉取资源(图片、网页、音频等)。如果这些 URL 源于用户输入或模型输出,就构成了 SSRF(服务器端请求伪造)的攻击面。攻击者可能利用此漏洞访问内网服务(如 http://127.0.0.1:5099/api)或云厂商元数据接口(如 http://169.254.169.254/latest/meta-data/iam/security-credentials/)。

dsh-netguard 是一个 DSH 插件,旨在作为“出网护栏”。它通过一个 netguard 服务和一个可选的 fetch_url 工具,统一管理所有出网请求的地址合法性、大小与超时,避免因判定逻辑写错而导致的内网暴露。

核心功能

该插件提供以下能力:

  • SSRF 防护:只允许访问公网地址,拦截私网、内网及特殊用途地址(如 127.0.0.1、169.254.169.254 等)。
  • DNS 解析检查:对 DNS 解析出的每一条地址都进行合法性判定,防止域名解析到内网。
  • 重定向逐跳复查:对 HTTP 重定向的每一跳地址重新进行体检,防止通过 302 位置头指向内网。
  • 流式大小限制:边读边计数,超过 maxBytes 立即断开连接,防止内存耗尽攻击。
  • 超时控制:整条请求链路共用一个超时预算,防止长时间卡顿或跳转拖慢整体性能。
  • 端口黑名单:拦截常见内网服务端口(如 22, 23, 25, 6379, 27017)。
  • IPv4/IPv6 地址分类:支持 IPv4 的各种变形写法(如 0x7f.1),IPv6 仅允许 2000::/3。
  • 工具支持:提供 fetch_url 工具(默认关闭),供模型调用获取公网内容。

安装与配置

安装

在 DSH 项目目录下执行:

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

配置

在插件配置文件中添加 netguard 服务实例。以下为常用参数示例:

- insert:
    - id: netguard
      name: dsh-netguard
      config:
        allowedProtocols: ['http:', 'https:']   # 仅允许 HTTP/HTTPS
        maxBytes: 8388608                       # 一次最多下载 8 MiB
        onOverflow: 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 工具

典型用法

给其他插件使用

获取公网字节(适合下载图片或音频):

const netguard = ctx.get('netguard')
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, bytes, truncated, redirects 等字段

仅体检 URL 合法性,不下载:

const u = await netguard.assertPublicUrl(url)   // 不安全会抛出 NetguardError

主机名与地址判定(可单独使用):

// 同步检查,不查 DNS,只挡明显写法
netguard.isPublicHost('127.0.0.1')        // false

// 异步检查,查 DNS
await netguard.checkHost('demo.example.com')  // 返回 { ok, addresses, offender? }

// IP 分类
netguard.classifyIp('::ffff:10.0.0.1')    // 返回 { isIp:true, public:false, reason:'PRIVATE' }

给模型使用(默认关闭)

若需允许模型调用外部接口获取内容,需在配置中开启工具:

config:
  enableTool: true

开启后,模型可使用 fetch_url 工具:

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

纯函数逻辑

如果不需要 DSH 环境,可直接引入插件提供的纯函数逻辑:

import { isPublicIp, classifyIp } from 'dsh-netguard/src/ip.js'
import { assertPublicUrl } from 'dsh-netguard/src/guard.js'

注意事项

插件虽然提供全面的地址检查,但在以下场景下存在局限:

  • DNS 重绑定(TOCTOU)攻击:插件在解析阶段和连接阶段可能使用不同的 DNS 结果(如 TTL=0 的域名),攻击者可能利用此时间差绕过检测。
  • 代理判定时机:插件的地址判定发生在代理配置之前,无法感知通过代理访问的实际地址。
  • 内容安全:插件只验证“能否去”和“拿多大”,不验证返回内容本身的安全性(如 HTML 中的恶意指令)。
  • 特殊跳转:不支持 <meta http-equiv="refresh"> 或 JavaScript 跳转,仅处理 HTTP 重定向。
  • 协议支持:不支持上传、POST 请求及流式长连接。
  • DNSSEC:不执行 DNSSEC 验证,依赖系统解析器。
  • 公网反代:攻击者若在公网服务器上搭建反向代理指向内网,地址层面无法检测。
  • 端口黑名单:黑名单为近似值,非完整列表(如 8081, 9090 等也可能有内网服务)。
  • IPv6 限制:仅允许 2000::/3,极少数合法的特殊 IPv6 写法可能会被误拦截。
  • 无速率限制:插件不限制请求的频率或并发数,需在外部自行添加配额控制。

总结

dsh-netguard 通过严格的地址分类、逐跳重定向检查和流式限流,为 DSH 插件提供了一个坚实的出网安全层。它解决了传统正则匹配无法覆盖的变形 IP、DNS 解析到内网以及重定向劫持等常见 SSRF 漏洞。开发者应根据实际业务需求调整 maxBytes、timeoutMs 和 blockedPorts 参数。

插件目录:SkillHub 插件目录
GitHub 仓库:JackZo400/dsh-netguard

羽毛球分组比赛记分
小程序二维码

欢迎使用《羽毛球分组比赛记分》微信小程序

小夜