devslab-kr/ssrf-guard-js

GitHub: devslab-kr/ssrf-guard-js

一款 JavaScript/TypeScript 的 SSRF 防护库,提供 URL 校验、DNS 检查、受保护的 fetch 以及 LLM 工具输入扫描等安全能力。

Stars: 0 | Forks: 0

# ssrf-guard-js [![npm](https://img.shields.io/npm/v/%40devslab%2Fssrf-guard-js)](https://www.npmjs.com/package/@devslab/ssrf-guard-js) [![CI](https://static.pigsec.cn/wp-content/uploads/repos/cas/ad/ad5834178f7599af9fdda11629d49cae07f2997beec49821b2920eff5bfd50e7.svg)](https://github.com/devslab-kr/ssrf-guard-js/actions/workflows/ci.yml) ![TypeScript](https://img.shields.io/badge/TypeScript-Ready-3178C6?logo=typescript&logoColor=white) [![License](https://img.shields.io/badge/License-Apache--2.0-blue)](./LICENSE) **[文档](https://devslab-kr.github.io/ssrf-guard-js/)** · [한국어](README.ko.md) JavaScript 和 TypeScript 的 SSRF 保护。 这是 [`devslab-kr/ssrf-guard`](https://github.com/devslab-kr/ssrf-guard) 的 JS/TS 对应版本。 它移植了相同的核心安全模型: - URL 阶段验证:scheme、host 允许列表、port、userinfo、IP 字面量检查 - 私有网络 IP 分类 - 用于隐藏 URL 的 LLM/tool-call JSON 扫描 - 带有 URL、DNS 和重定向检查的受保护的 fetch ## 安装 ``` pnpm add @devslab/ssrf-guard-js ``` 有关复制粘贴的教程,请参阅文档站点: https://devslab-kr.github.io/ssrf-guard-js/ ## URL 策略 ``` import { validateUrl } from '@devslab/ssrf-guard-js'; validateUrl('https://api.example.com/v1', { exactHosts: ['api.example.com'], allowedSchemes: ['https'], allowedPorts: [-1, 443], }); ``` 空的 `exactHosts` 和 `suffixes` 会默认拒绝(fail-closed):在您配置它们之前,不允许任何 host。 默认值: - `allowedSchemes`: `['http', 'https']` - `allowedPorts`: `[-1, 80, 443]` — 没有显式端口的 URL 会被视为该 scheme 的默认端口(`http`/`ws` → `80`,`https`/`wss` → `443`),因此仅有 `allowedPorts: [443]` 就足以实现仅限 HTTPS 的策略。`-1` 用于匹配没有已知默认端口的 scheme 的无端口 URL。 - `rejectIpLiteralHosts`: `true` - `rejectUserInfo`: `true` - `blockPrivateNetworks`: `true` ## LLM 工具输入防护 ``` import { guardToolInputJson } from '@devslab/ssrf-guard-js'; const violation = guardToolInputJson( JSON.stringify({ request: { target: 'http://169.254.169.254/latest/meta-data/' } }), { exactHosts: ['api.example.com'] }, ); if (violation) { return violation; // structured JSON error for the model/tool caller } ``` 该防护会遍历整个 JSON 树。隐藏在嵌套对象、数组或解释字段中的恶意 URL 也会被拦截。扫描器会收集任何 `scheme://` URL(策略的 `allowedSchemes` 决定最终结果,因此 `file://` 或 `gopher://` 默认会被拒绝)以及协议相对的 `//host` 字符串,并根据 host 策略对它们进行验证。 ## 受保护的 Fetch ``` import { safeFetch } from '@devslab/ssrf-guard-js'; const response = await safeFetch('https://api.example.com/data', { exactHosts: ['api.example.com'], allowedSchemes: ['https'], }); ``` `safeFetch` 会验证 URL,检查 DNS 结果中是否包含私有/本地 IP(如果任何解析到的地址是私有的,则会默认拒绝),并重新验证每一次重定向跳转。在跨源重定向时,它会移除 `Authorization`、`Proxy-Authorization` 和 `Cookie` header,并将 `303`(以及 `301`/`302` 的 `POST`)重定向降级为 `GET`,且不会重放请求体。 ### DNS pinning(可选) 默认情况下,DNS 检查和实际连接会分别解析 hostname,从而留下一个微小的 DNS-rebinding 窗口。安装可选的 [`undici`](https://www.npmjs.com/package/undici) 依赖即可关闭此窗口: ``` pnpm add undici ``` 当存在 `undici` 时,`safeFetch` 会自动**在 socket connector 内部**验证解析到的地址,从而使检查和连接共享同一次 DNS 解析——这与 Java Apache HttpClient adapter 使用的 socket 级别 pinning 相同。可以使用 `pinDns` 选项对其进行显式控制: ``` await safeFetch(url, policy, { pinDns: true }); // require pinning (throws without undici) await safeFetch(url, policy, { pinDns: false }); // force check-then-fetch ``` 如果没有 `undici`,`safeFetch` 将回退到 check-then-fetch 模式。这是一道强有力的防护栏,但在进行高风险的任意 URL 爬取时,请使用严格的允许列表或专用的 egress 服务。 ## 运行时支持:Node 与 Cloudflare Workers 并非所有的保证都能在所有运行时中保留。请了解您所使用的运行环境支持哪些功能: | 功能面 | Node | Cloudflare Workers | | --- | --- | --- | | `validateUrl` / `UrlPolicy` / `HostPolicy` | ✅ | ✅ (纯 URL/字符串检查) | | `guardToolInput` / `guardToolInputJson` / `createGuardedToolHandler` | ✅ | ✅ | | `guardedFetch` + `sameSitePolicy` (URL 阶段 + 重定向重新验证) | ✅ | ✅ | | `safeFetch` (增加 DNS 检查) | ✅ | ❌ 运行时抛出错误 | | 通过 `undici` 实现 DNS pinning | ✅ 可选 | ❌ | **为什么 `safeFetch` 无法在 Workers 中运行。** 它在建立连接之前使用 `node:dns/promises` 的 `lookup` 来解析目标地址。Workers 的 `node:dns`(在 `nodejs_compat` 标志下)通过 DNS-over-HTTPS 实现了 `resolve*` 函数,但 `lookup` 会抛出 `Not implemented` 错误——即使它能成功解析,Workers 的 `fetch` 也会在内部执行自己的解析,因此用户态代码无法像 `undici` connector 在 Node 中那样,将检查过的 IP 绑定到 socket 上。在 Worker 内部无法消除 check-then-fetch 的间隙。(导入此包始终是安全的——`node:dns` 是延迟加载的;在没有它的情况下调用 `safeFetch` 会抛出一个指向此处的特定 `SsrfGuardError` 错误。) **在 Workers 中应该使用什么:`guardedFetch`。** 具有与 `safeFetch` 相同的重定向重新验证、凭据移除和方法降级语义,但去除了 DNS 检查——因此策略允许列表是主要的控制手段,而默认拒绝(fail-closed)机制(即空的允许列表不允许任何内容)正在发挥实际作用: ``` import { guardedFetch } from '@devslab/ssrf-guard-js'; const res = await guardedFetch('https://api.example.com/data', { exactHosts: ['api.example.com'], allowedSchemes: ['https'], }); ``` 要开放特定的 host,请将其放入允许列表——这就是豁免机制,并且它会集中在一个可审计的地方。对于“抓取客户自有站点”的流程,可以使用 `sameSitePolicy` 从提交的 URL 中派生允许列表:整个 fetch 过程——包括重定向——都被锁定在该域上(移除 `www.`,以便支持 apex ↔ www 重定向): ``` import { guardedFetch, sameSitePolicy } from '@devslab/ssrf-guard-js'; const input = 'https://www.customer-site.example/about'; const res = await guardedFetch(input, sameSitePolicy(input, { allowedSchemes: ['https'] })); ``` 如果确实需要在 Workers 中进行任意 URL 抓取,请通过一个小型基于 Node 的 egress 服务来路由请求,该服务调用设置了 `pinDns: true` 的 `safeFetch`——Worker 直接与该 egress 服务通信,绝不直接连接用户提供的 URL。 ## Express ``` import express from 'express'; import { createExpressUrlGuard } from '@devslab/ssrf-guard-js'; const app = express(); app.use(express.json()); app.post( '/crawl', createExpressUrlGuard({ exactHosts: ['example.com'], suffixes: ['example.com'], allowedSchemes: ['https'], }), async (req, res) => { res.json({ ok: true }); }, ); ``` 该中间件默认扫描 `req.body` 和 `req.query`。当发现被拦截的 URL 时,它会返回一个结构化的 `400` 响应。 ## Vite 当您的 Vite 开发服务器具有接收 URL 并在服务端对其进行 fetch 的 SSR/proxy endpoint 时,可以使用此项。 ``` // vite.config.ts import { defineConfig } from 'vite'; import { ssrfGuardVitePlugin } from '@devslab/ssrf-guard-js/vite'; export default defineConfig({ plugins: [ ssrfGuardVitePlugin({ routes: ['/api/crawl'], policy: { suffixes: ['example.com'], allowedSchemes: ['https'], }, }), ], }); ``` 该插件默认扫描名为 `url`、`target`、`uri` 和 `href` 的查询参数。被拦截请求的示例: ``` /api/crawl?url=http://169.254.169.254/latest/meta-data/ ``` ## LangChain / Agent 工具 `createGuardedToolHandler` 可以为任何对象输入的 tool function 提供包装,而无需强依赖于 LangChain。 ``` import { DynamicStructuredTool } from '@langchain/core/tools'; import { z } from 'zod'; import { createGuardedToolHandler, safeFetch } from '@devslab/ssrf-guard-js'; const policy = { suffixes: ['example.com'], allowedSchemes: ['https'], }; export const fetchUrlTool = new DynamicStructuredTool({ name: 'fetch_url', description: 'Fetch an allowed URL', schema: z.object({ url: z.string().url() }), func: createGuardedToolHandler(policy, async ({ url }) => { const response = await safeFetch(url, policy); return await response.text(); }), }); ``` 如果模型试图传递私有 IP、metadata URL 或未允许的 host,该工具将返回结构化的 `ssrf_blocked` JSON 字符串,而不是执行抓取。 ## 拦截原因 抛出的 `SsrfGuardError` 实例会暴露稳定的 `reason` 值: - `blocked_scheme` - `blocked_host` - `blocked_port` - `blocked_ip_literal` - `blocked_userinfo` - `blocked_private_ip` - `blocked_redirect` - `blocked_other` ## 维护者发布 发布由 GitHub Actions 处理。 1. 添加一个 npm automation token 作为仓库的 secret `NPM_TOKEN`。 2. 在 `package.json` 中更新 `version`。 3. 提交更改。 4. 创建并推送匹配的 tag,例如: ``` git tag v0.4.0 git push origin main --tags ``` `Publish to npm` workflow 会对包进行验证,检查 tag 是否与 `package.json` 匹配,然后运行: ``` npm publish --access public --provenance ``` ## 项目家族 - [ssrf-guard](https://github.com/devslab-kr/ssrf-guard) — JVM 对应版本:为 Spring Boot 提供相同的安全模型,涵盖 9 个 HTTP-client 模块,包括用于 LLM-agent tool URL 验证的 `-springai` / `-langchain4j` - 来自 [devslab](https://github.com/devslab-kr) 的更多开源项目 ## License [Apache-2.0](./LICENSE)
标签:CISA项目, CMS安全, JavaScript, MITM代理, SSRF防护, TLS, TypeScript, Web安全, 安全插件, 数据可视化, 程序员工具, 网络安全, 自动化攻击, 蓝队分析, 防御工具, 隐私保护