Spyyy004/owthorize
GitHub: Spyyy004/owthorize
一个同步的 JS/TS 安全防护库,在 AI Agent 工具调用执行前通过 AST 级语义解析拦截破坏性操作,防御提示注入、参数幻觉和推理错误带来的安全风险。
Stars: 1 | Forks: 0
# owthorize
[](https://www.npmjs.com/package/owthorize)
[](./LICENSE)
[](./dist/index.d.ts)
[](./package.json)
`owthorize` 是一个同步的 JS/TS 库,位于 AI agent 和您的系统(数据库、HTTP、文件系统、shell)之间,能在**破坏性的工具调用执行前将其拦截**。
它通过将 SQL 解析为 AST、规范化 URL、对 shell 命令进行分词来理解调用的真正含义,而不是简单地匹配字符串模式。这一区别至关重要:正则表达式会漏掉 `WHERE 1=1`、带 schema 限定的表名以及 IPv4 映射的 IPv6 地址。而 owthorize 不会。
```
Your agent calls a tool
│
▼
owthorize parses the payload into a typed shape
│
▼
Rules evaluate the parsed shape
│
┌────┴────┐
allow deny
│ │
handler GuardDenied (your existing error handling catches it)
runs
```
## 为什么会有这个项目
AI agent 会调用工具。而工具会带来副作用。在生产环境中通常会出现以下三种问题:
**1. 提示注入。** agent 读取的某些内容(GitHub issue、文档、网页)中包含隐藏指令,迫使模型发起开发人员从未预料到的调用。
**2. 参数幻觉。** 模型生成了语法有效但包含错误 payload 的工具调用——例如,用 `DELETE FROM users` 代替了 `DELETE FROM users WHERE id = ?`。
**3. 推理错误。** 模型试图“提供帮助”——运行未被要求的破坏性清理操作,获取不应访问的内部 IP,或者写入其工作空间之外的位置。
在上述三种情况下,提示级别的安全防护(“你是一个有用的助手,不要删除表”)毫无作用。故障发生在工具调用层面,而不是提示层面。这正是 owthorize 发挥作用的地方。
## 安装
```
npm install owthorize
```
Node >= 18。同时支持 ESM 和 CJS。
## 快速开始
```
import { Guard, rules, GuardDenied } from "owthorize"
const guard = new Guard({
rules: [
rules.sql.denyDDL(), // block DROP, ALTER, TRUNCATE, etc.
rules.sql.denyMutationWithoutWhere(), // block DELETE/UPDATE with no WHERE
rules.http.denyHosts(rules.http.SSRF_DEFAULTS), // block internal IPs, AWS metadata
],
})
// Wrap your tool handler once. owthorize intercepts every call.
const safeQuery = guard.tool("db.query", {
adapter: "sql.postgres",
handler: async ({ query }: { query: string }) => db.query(query),
})
// This gets blocked before it ever reaches your database:
try {
await safeQuery({ query: "DROP TABLE users" })
} catch (err) {
if (err instanceof GuardDenied) {
console.log(err.matched, "->", err.reason)
// sql.denyDDL -> DDL not allowed: drop
}
}
// This runs normally:
await safeQuery({ query: "SELECT id FROM users WHERE id = $1" })
```
### 测试您的规则,无需触碰真实系统
```
const result = guard.simulate("db.query", { query: "DROP TABLE users" })
// { decision: "deny", matched: "sql.denyDDL", reason: "DDL not allowed: drop", irreversible: true }
```
`guard.simulate()` 会运行完整的评估管道,但绝不会调用您的处理程序。您可以在测试套件中使用它来断言规则行为,且不会产生任何副作用。
## 它能拦截什么
| 类别 | 被拦截的示例 |
|---|---|
| SQL DDL | `DROP TABLE`、`TRUNCATE`、`ALTER TABLE`、`CREATE`、`RENAME` |
| 无限制的变更 | 不带 `WHERE` 的 `DELETE FROM users`,不带 `WHERE` 的 `UPDATE users SET ...` |
| SSRF 目标 | `169.254.169.254`(AWS 元数据)、`192.168.x.x`、`localhost`、`*.internal`、IPv4 映射的 IPv6 |
| 危险的 shell | `rm -rf`、管道滥用、反引号 / `$()` 替换、shell 元字符 |
| 路径穿越 | 任何解析到已配置的根目录之外的路径 |
| 自定义策略 | 项目所需的任何内容——类型化的规则函数,而非 Rego 或正则表达式字符串 |
## 工作原理:适配器
当您的 agent 调用工具时,owthorize 会在规则评估 payload 之前,先将其通过一个**适配器**。适配器将原始输入转换为类型化的结构化形态。规则看到的是这种形态——而不是原始字符串。
| 适配器 | 您的工具接收的内容 | 规则看到的内容 |
|---|---|---|
| `sql.postgres` / `sql.mysql` / `sql.sqlite` | `{ query, params? }` | `kind`、`tables`、`hasWhere`、`ddlOp`、`dialect` |
| `http` | `{ url, method?, headers?, body? }` | 解析后的 URL(包含 IPv4 映射的 IPv6 规范化),小写的 header 键 |
| `shell` | `{ command }` 或 `{ argv }` | 分词后的 argv、元字符 / 管道 / 重定向 / 替换标志 |
| `fs` | `{ path, op? }` | 规范化的绝对路径,操作类型 |
| `raw` | 任何内容 | 透传——与跨适配器的自定义规则结合使用 |
这是它与正则表达式的核心区别。像 `denyMutationWithoutWhere()` 这样的规则不会去字符串里搜索单词“WHERE”——它会检查解析后的 SQL AST 是否包含 WHERE 节点。`DELETE FROM users WHERE 1=1` 包含一个 WHERE 节点;它能通过此规则。这是符合预期的行为,并在 USAGE.md 中有详细说明。
## 内置规则
```
// SQL
rules.sql.denyDDL() // DROP, TRUNCATE, ALTER, CREATE, RENAME
rules.sql.denyMutationWithoutWhere() // UPDATE/DELETE with no WHERE clause
rules.sql.denyTables({ deny: ["users"] }) // block writes to specific tables
rules.sql.denyTables({ allow: ["logs"] }) // only allow writes to specific tables
// HTTP
rules.http.denyHosts(rules.http.SSRF_DEFAULTS) // RFC1918, loopback, AWS metadata, *.internal
rules.http.denyHosts(["evil.com", "10.0.0.0/8"]) // custom host/CIDR denylist
rules.http.allowHosts(["api.stripe.com"]) // allowlist — everything else is denied
// Shell
rules.shell.denyCommands(["rm", "curl", "wget", "nc"])
// Filesystem
rules.fs.confineTo(["/tmp/agent-workspace"]) // deny anything outside this root
// Custom — typed per adapter, so your rule gets autocomplete on the parsed shape
rules.sql.custom({
name: "no-payments-after-hours",
when: ({ parsed }) => parsed.tables.includes("payments") && new Date().getHours() >= 17,
decide: () => deny("payments table is read-only after 5pm", "policy.payments-window"),
})
```
所有会拦截不可逆操作的内置规则(`denyDDL`、`denyMutationWithoutWhere`、破坏性的 shell 命令、根目录之外的 fs 写入)都会在拒绝结果中设置 `irreversible: true`。自定义规则也可以选择启用此特性。
## `irreversible` 标志
并非所有的拒绝都同等严重。拦截对受限表的 `SELECT` 与拦截 `DROP TABLE` 是不同的。`irreversible` 标志允许您在自己的代码中以不同方式路由它们——对大多数情况进行静默拒绝,对那些无法撤销的操作要求人工审批:
```
const result = guard.simulate("db.query", payload)
if (result.decision === "allow") return safeQuery(payload)
if (result.irreversible) return slackBot.requestApproval(result)
return res.status(403).json({ matched: result.matched })
```
owthorize 绝不会为了等待审批而阻塞。它同步返回决策结果,并让您的代码决定后续操作。
## 框架集成
只需一次调用即可包装您的整个工具注册表。owthorize 会保留所有框架特定的字段,仅拦截处理程序。
**OpenAI**
```
import { protectTools } from "owthorize/openai"
const safeTools = protectTools(guard, openaiTools, {
db_query: { adapter: "sql.postgres" },
fetch_url: { adapter: "http" },
})
// Pass safeTools to client.chat.completions.create({ tools: safeTools, ... })
```
**Vercel AI SDK**
```
import { protectTools } from "owthorize/vercel-ai"
const safeTools = protectTools(guard, tools, {
db_query: { adapter: "sql.postgres" },
})
await generateText({ model, tools: safeTools, prompt })
```
**Anthropic SDK**
```
import { protectTools } from "owthorize/anthropic"
const safeTools = protectTools(guard, tools, { db_query: { adapter: "sql.postgres" } })
```
**LangChain JS**
```
import { protectTools } from "owthorize/langchain"
const safeTools = protectTools(guard, tools, { db_query: { adapter: "sql.postgres" } })
```
没有处理程序的工具(纯 schema / 客户端工具)将直接透传,不受影响。
## 审计日志
每次评估——无论允许还是拒绝,无论真实调用还是模拟调用——都会写入一条结构化记录。默认情况下,它会输出到 `console.log`。在生产环境中,请将其指向您自己的日志记录器:
```
const guard = new Guard({
rules: [...],
audit: {
sink: (record) => logger.info({ owthorize: record }),
},
})
```
每条记录的格式如下:
```
{
"ts": "2026-05-01T12:00:00Z",
"tool": "db.query",
"adapter": "sql.postgres",
"parsed": { "kind": "ddl", "ddlOp": "drop", "tables": ["users"], "hasWhere": false },
"payload_hash": "sha256:898f...",
"decision": "deny",
"matched_rule": "sql.denyDDL",
"matched_rule_kind": "builtin",
"reason": "DDL not allowed: drop",
"irreversible": true,
"simulated": false
}
```
敏感字段会在哈希处理之前被剥离:
```
guard.tool("db.query", {
adapter: "sql.postgres",
handler: myHandler,
redact: ["params.password", "params.apiKey"],
})
```
如果您在测试中希望实现零输出:
```
import { Guard, silentSink } from "owthorize"
const guard = new Guard({ audit: { sink: silentSink }, rules: [...] })
```
## 故障模式与默认行为
当评估管道本身出现问题时会发生什么:
| 情况 | 默认行为 | 覆盖配置 |
|---|---|---|
| 工具未通过 `guard.tool()` 包装 | deny(拒绝) | `defaults.onUnknownTool: "allow"` |
| 规则抛出异常 | deny(拒绝) | `defaults.onRuleError: "allow"` |
| 适配器无法解析 payload | deny(拒绝) | `defaults.onAdapterError: "allow"` |
| 审计输出端抛出异常 | continue(继续,写入后备输出端) | `defaults.onLogError: "throw"` |
默认策略始终是“失败即拒绝”。您可以在原型设计阶段放宽各项默认设置:
```
const guard = new Guard({
rules: [...],
defaults: {
onUnknownTool: "allow", // useful while you're still wrapping everything
},
})
```
一旦您包装了所有需要控制的工具,请务必将 `onUnknownTool` 恢复为 `"deny"`(默认值)。
## 威胁模型
**owthorize 能拦截:** 提示注入的工具调用、参数幻觉、agent 推理错误,以及已知的不安全 payload 形态(DDL、无限制的变更、SSRF 目标、shell 元字符、路径穿越)。
**owthorize 无法拦截:** 完全绕过 SDK 的恶意 agent 运行时、您自己的工具处理程序代码中的漏洞,或者在工具边界之前发生的副作用。信任边界在于包装层——您没有包装的,就无法进行控制。
为了防御恶意的运行时,您需要一个进程边界:例如代理、sidecar 或容器出站规则。这属于一个独立的层级,也需要不同的产品来实现。
## 设计原则
**解析,而不是匹配。** 规则评估的是 AST 和已解析的结构,而不是字符串。对 SQL 使用正则表达式是一个缺陷生成器——它会对那些看起来与模式作者预期略有不同的有效 SQL 产生漏报。
**同步。** 允许或拒绝,立即返回。没有 webhooks,SDK 中也没有内置异步审批状态机。`irreversible` 标志允许您将其路由到自己的审批流程,而无需 SDK 了解相关细节。
**遇到不确定情况时默认拒绝。** 未知工具、解析器失败、规则异常——默认全部拒绝。失败时保持开放是一种安全反模式。
**一次包装。** `guard.tool(name, handler)` 就是完整的 API 接口。您无需在处理程序中根据 `decision === "allow"` 进行分支判断。
**从第一天起就具备可测试性。** `guard.simulate()` 是一等公民 API,而非事后补充。每个内置规则都随附了测试用例。
## 常见问题
**审计日志在启动时刷屏 stdout。** 这是故意的——默认可见意味着您不可能在不经意间发布一个没有审计跟踪的服务。可以传递一个自定义的 `sink` 或 `silentSink` 来抑制它。
**`onUnknownTool: "deny"` 是默认设置。** 如果您调用了一个忘记包装的工具,您会得到 `GuardDenied: unknown tool`。在开发期间可以通过 `defaults.onUnknownTool: "allow"` 放宽此限制,开发完成后记得将其设置回去。
**适配器期望特定的 payload 键。** `sql.*` 期望接收 `{ query }`。`http` 期望接收 `{ url }`。`fs` 期望接收 `{ path }`。`shell` 期望接收 `{ command }` 或 `{ argv }`。如果您的处理程序使用了不同的键名,请编写一个简单的包装器来重命名它们,或者设置 `adapter: "raw"` 并使用自定义规则。
**`hasWhere` 是结构性的,而非语义上的。** `DELETE FROM users WHERE 1=1` 在 AST 层面包含一个 WHERE 子句,因此 `denyMutationWithoutWhere()` 会放行它。如果需要对特定表提供更强的保护,请将其与 `denyTables` 结合使用。
**`fs.confineTo` 不会跟踪符号链接。** 位于允许的根目录内、但指向根目录外部的符号链接不会被拦截。如果您需要这种防御,请在处理程序中调用 `fs.realpath`。
**`http.denyHosts` 只拦截 IP 字面量,不拦截 DNS 名称。** 它不会解析主机名。若要防御 DNS 重绑定攻击,请在出站代理后运行。
## 当前状态
v0.4.1 —— 公共 API 已趋稳定,但在收集到来自原作者之外的现场反馈之前,版本号将保持在 1.0 以下。
**已端到端验证:** SQL 适配器与规则、审计日志、`guard.simulate()`、OpenAI 适配垫片、Vercel AI 适配垫片——均已针对真实的 Express + Drizzle + MySQL 后端进行了测试。
**已构建并经过单元测试,但尚未进行现场验证:** HTTP 适配器、SSRF 规则、Anthropic 适配垫片、LangChain 适配垫片、shell 适配器、FS 适配器。
**迈向 v1.0 的路线图:**
- 基于外部用户反馈进行 API 稳定化
- LangChain 和 Anthropic 适配垫片的端到端现场运行
- 审批流的最佳实践(Slack、队列)以文档模式提供
**不在 v1.0 的路线图内** —— 这些将破坏同步模型:
- 内置的人工审批 UI 或状态机
- 托管的策略服务器或多租户控制平面
- 需要运行查询才能得出的行数估计或影响范围估计
## 文档
- [USAGE.md](./USAGE.md) —— 完整指南:自定义规则、框架连接、审计日志配置、故障模式
- [FIELD-TESTING.md](./FIELD-TESTING.md) —— 每个公开接口的验证状态
- [field-report.md](./field-report.md) —— 针对真实流量测试内容的运行日志
## 贡献
带有最小复现步骤的错误报告是您能提供的最有价值的东西。如果记录到了失败调用的审计日志,请将其附上——它能准确指出评估管道在何处出了问题。
```
git clone https://github.com/Spyyy004/owthorize.git
cd owthorize
npm install
npm run typecheck
npm run lint
npm test
npm run build
```
示例脚本(`npm run example`、`npm run example:openai`、`npm run example:vercel-ai`)中的框架适配垫片示例需要一个 `OPENAI_API_KEY`。普通的快速开始示例无需任何 API 密钥即可运行。
## 许可证
MIT
标签:AI安全, AI安全网关, AI防火墙, AST解析, Chat Copilot, CISA项目, CMS安全, JavaScript, LLM安全防护, MITM代理, npm包, Prompt注入防御, RASP, Shell元字符过滤, SQL注入防御, SQL解析, SSRF防御, TypeScript, 同步拦截, 命令注入防御, 大模型安全, 安全中间件, 安全插件, 安全网关, 工具调用拦截, 幻觉防御, 数据可视化, 暗色界面, 纵深防御, 自动化攻击, 路径遍历防御