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
[](https://www.npmjs.com/package/@devslab/ssrf-guard-js)
[](https://github.com/devslab-kr/ssrf-guard-js/actions/workflows/ci.yml)

[](./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安全, 安全插件, 数据可视化, 程序员工具, 网络安全, 自动化攻击, 蓝队分析, 防御工具, 隐私保护