bartolli/vaglio

GitHub: bartolli/vaglio

一个零依赖的确定性文本净化库,在 LLM 信任边界处防御 Unicode 提示注入攻击并编校敏感凭据。

Stars: 0 | Forks: 0

# vaglio 净化跨越 LLM 信任边界的文本。 位于提取内容、工具输出、RAG 块、对等模型输出与模型之间的文本域过滤器。零运行时依赖,仅支持 ESM,流式感知,确定性。 ## 安装 ``` pnpm add @bartolli/vaglio ``` 需要 Node ≥ 22 LTS。仅支持 ESM。 ## 快速开始 ``` import { sanitize } from '@bartolli/vaglio'; const safe = sanitize(untrustedText); await llm.send(safe); ``` 流水线顺序:NFKC → 剥离 Unicode 不可见字符 → 剥离推理标签块 → 编校凭据。所有四个阶段均针对 [`DEFAULT_POLICY`](#default-policy) 运行。 ## 威胁覆盖范围 确定性。无机器学习,无熵启发式算法。 - **Unicode 不可见字符。** 标签块 (U+E0001–U+E007F)、零宽度 (ZWSP, ZWNJ, BOM, word joiner)、双向覆盖 (U+202A–U+202E, U+2066–U+2069)、蒙古文自由变体选择器、行间注释、补充区 PUA、补充区变体选择器、软连字符 / CGJ / 谚文填充符、不可见数学运算符、孤立的 UTF-16 代理项。 - **通过 NFKC 进行的同形字伪造。** 数学字母数字符号 (𝐬𝐲𝐬𝐭𝐞𝐦 → system) 和全角字符 (<system> → \) 折叠为 ASCII。跨脚本 (希腊字母↔拉丁字母, 西里尔字母↔拉丁字母) 根据规范保留。 - **Zalgo 文本。** 每个基础字符的组合标记上限 (默认为 4)。 - **ANSI 转义序列。** `ESC[…]` 序列 — 在终端中不可见,但被 LLM 逐字按原样分词。 - **C0/C1 控制字符。** 剥离 U+0000–U+001F 和 U+007F–U+009F,除了 `\t`, `\n`, `\r`。NBSP (U+00A0) 被保留 (可打印,非控制字符)。 - **推理标签泄漏。** `` 块;标签名称集可配置。 - **凭据。** Anthropic, AWS (AKIA/ASIA), Bearer JWT, Slack, GitHub PAT, Stripe restricted, PEM 私钥 (RSA / EC / Ed25519 / 通用), 长十六进制字符串 (≥ 64 字符)。默认占位符 ``。 ## 插入位置 ``` URL → fetch → HTML→markdown extractor → markdown ┐ File → document extractor (PDF, DOCX, …) → text │ HTML → HTML→text converter → text │ RAG → retrieve → chunk → text ├→ sanitize() → LLM Tool → API call → serialize → string │ LLM → output (router / planner / loop step) → text ┘ ``` 最后一行是模型间的信任边界。接收模型将对等模型输出视为不受信任的 — 与用户输入具有相同的攻击面。 ## 实用方案 ### 净化字符串 ``` import { sanitize } from '@bartolli/vaglio'; const safe = sanitize(text); ``` ### 通过回调进行遥测 除非提供 `onFinding`,否则非 `Detailed` 变体不会构建 findings 数组。静默操作是默认代价。 ``` import { sanitize } from '@bartolli/vaglio'; sanitize(text, { onFinding: (f) => metrics.emit(f.kind, f.ruleId, f.severity), }); ``` ### Detail 变体 ``` import { sanitizeDetailed } from '@bartolli/vaglio'; const result = sanitizeDetailed(input); if (result.text === input) return input; // identity-check fast path (contract) audit.write(result.findings); return result.text; ``` ### Web Streams 跨块的凭据编校通过内部滑动窗口缓冲区 (`Policy.bufferLimit`,从最长活跃模式 + 64 自动推导) 进行。 ``` import { createSanitizeStream } from '@bartolli/vaglio'; await response.body! .pipeThrough(new TextDecoderStream()) .pipeThrough(createSanitizeStream({ onFinding: emitMetric })) .pipeTo(modelInputSink); ``` ### Async iterable ``` import { sanitizeIterable } from '@bartolli/vaglio'; async function* turns() { for await (const turn of agentLoop) yield turn.content; } for await (const safe of sanitizeIterable(turns(), { onFinding: emitMetric })) { yield safe; } ``` ### 自定义凭据模式 ``` import { policy, sanitize } from '@bartolli/vaglio'; const myPolicy = policy() .addCredentialPattern(/sot-session-[a-z0-9]{32}/i, { ruleId: 'sot-session', placeholder: '', severity: 'high', }) .build(); const safe = sanitize(text, { policy: myPolicy }); ``` 构建器是不可变的:每个方法都返回一个新实例;`build()` 返回一个冻结的 `Policy`。重用基础策略以派生变体: ``` const base = policy().addReasoningTag('plan'); const stricter = base.disableUnicodeCategory('soft-hyphen-fillers').build(); const lenient = base.build(); ``` ### 模型到模型链中的逐跳策略 ``` import { policy, sanitize } from '@bartolli/vaglio'; const planner = policy().addReasoningTag('scratchpad').addReasoningTag('plan').build(); const router = policy().build(); const safeRouterOut = sanitize(routerOutput, { policy: router }); await mainModel.generate({ context: safeRouterOut }); const safePlannerOut = sanitize(plannerOutput, { policy: planner }); await worker.run(safePlannerOut); ``` ### 低延迟流式传输 默认的 `bufferLimit` 是 **4160** 个字符 — 由 PEM 私钥模式 (`maxMatchLength: 4096` + 64) 驱动。按大约 4 个字符/token 计算,下游会看到约 1k token 的暂留数据。如果不接收 PEM 块,对于 token 流代理可以去除 PEM 模式: ``` import { policy, createSanitizeStream } from '@bartolli/vaglio'; const lowLatency = policy().removeCredentialPattern('pem-private-key').build(); // bufferLimit = 320 (~80 tokens) const stream = createSanitizeStream({ policy: lowLatency }); ``` ### 工具结果与 RAG ``` import { sanitize } from '@bartolli/vaglio'; const apiResponse = await externalTool(args); const safeToolResult = sanitize(JSON.stringify(apiResponse)); const chunks = await retriever.search(query); const context = chunks.map((c) => sanitize(c.text)).join('\n\n'); ``` ## Findings ``` type Finding = | { kind: 'unicode-strip'; ruleId: string; ruleVersion: number; action: PolicyAction; offset: number; length: number; charClass: string; count: number; severity: Severity; } | { kind: 'credential'; ruleId: string; ruleVersion: number; action: PolicyAction; offset: number; length: number; placeholder: string; severity: Severity; } | { kind: 'stream-diagnostic'; ruleId: string; ruleVersion: number; severity: Severity; message: string; }; type PolicyAction = 'stripped' | 'redacted' | 'replaced'; type Severity = 'low' | 'medium' | 'high' | 'critical'; ``` 在 v0.1 中,`unicode-strip` 和推理标签的 findings 会发出 `action: 'stripped'`;凭据的 findings 会发出 `action: 'redacted'`。`'replaced'` 被保留未使用。 Findings 不携带原始片段 — 将部分凭据或 PII 发送到遥测中是一种二次泄漏的反模式。 当内置项的 pattern、severity 或 `charClass` 发生变化时,`ruleVersion` 会随 CHANGELOG 递增。取证日志可在模式更新后继续存活。 连续相同的违规行为 (Zalgo 连续字符、重复的标签块码点) 会通过 `count` 和 `length` 聚合为单个 finding。 ## 默认策略 `DEFAULT_POLICY`: | 位置 | 默认值 | | -------------------- | --------------------------------------- | | Unicode categories | 启用所有 13 种 (见下文) | | Combining-mark cap | 每个基础字符 4 个 | | NFKC | 启用 | | Reasoning tags | `internal` | | Credential patterns | 8 个内置项 | | Placeholder | `` | | `bufferLimit` | 4160 (由 PEM 驱动) | 类别:`tags-block`, `zero-width`, `bidi-override`, `mongolian-fvs`, `interlinear-annotations`, `object-replacement`, `supplementary-pua`, `supplementary-variation-selectors`, `soft-hyphen-fillers`, `math-invisibles`, `orphaned-surrogates`, `ansi-escapes`, `c0-c1-controls`。 每个位置都可以通过构建器进行调整。 ## 流式传输契约 - 在批处理 (`sanitize`, `redact`)、Web Streams (`createSanitizeStream`, `createRedactStream`) 和异步可迭代对象 (`sanitizeIterable`, `redactIterable`) 之间具有一致的语义。 - 滑动窗口缓冲区在每次推送时保留尾部 `bufferLimit` 个字符,以便跨块的凭据能够落入下一个前导区域。 - 溢出:当保留的尾部在不丢失匹配项的情况下无法缩小时,最旧的字节将被提交,并触发一个 `stream-diagnostic` finding (`ruleId: 'buffer-overflow-warning'`)。编校优先于保真度。 - 取消:`TransformStream.cancel(reason)` 或 async-iter 的 `return()` 会释放缓冲区并丢弃部分凭据状态。随后的操作会抛出 `VaglioStreamCanceledError`。如果已订阅,最终的 `stream-diagnostic` finding (`ruleId: 'stream-canceled'`) 将通过 `onFinding` 发出。 - `flush()` 是幂等的。双重 flush 是空操作。在 `flush()` 之后调用 `push()` 会抛出异常。 - 错误是快速失败的:灾难性的正则表达式回溯或损坏的状态会导致流崩溃。消费者不得重试。 - 身份保留不适用于流式传输 — 如果需要引用相等性快速路径,请使用批处理接口。 ## 路线图 计划在 v0.2 及更高版本中推出: - **跨脚本同形字折叠** (希腊字母↔拉丁字母, 西里尔字母↔拉丁字母) 作为可选的 `Policy` 标志。默认关闭 — 跨脚本的折叠会破坏合法的非拉丁文本。 - **ReDoS 静态分析**,在 `Policy.build()` 时应用于用户提供的模式。v0.1 内置项已在库构建时预验证;用户提供的模式则被直接接受而未检查。 - **Detail-variant 流。** 流式传输在 v0.1 中仅公开 `onFinding`;`createSanitizeStreamDetailed` 形式正在考虑中。 - **可插拔的 Unicode 规则** — `addUnicodeRule({ ruleId, range, action })`,用于在封闭的 `UnicodeCategory` 集之外进行码点范围剥离。v0.1 中的替代方案:使用正则表达式的 `addStripPattern`。 - **控制 token 伪造检测** — 特定于模型的消息角色标记 (`<|im_start|>`, `Human:` / `Assistant:` 等)。需要针对每个模型的预设。 - **外部内容标记** — 自动将 RAG 输出包装在 `` 中。 ## 超出范围 以下内容属于其他层,不属于 vaglio: - **HTML 解析。** Vaglio 仅处理文本和 Markdown。请在上游运行 HTML 净化器。 - **内容提取。** Vaglio 净化已提取的文本,而不是原始文档。请在上游运行提取器。 - **二进制 / EXIF 剥离。** 不同的领域 (图像 / 文件)。 - **特定通道的输出格式化。** 应用策略 — 聊天平台渲染、表情符号允许列表、长度上限、大小写规范化。 - **非确定性检测。** 重复/熵启发式算法、基于机器学习的检测、间接提示注入 (IPIA) 防御、token 边界破坏缓解。Vaglio 在契约上是确定性的 — 这些属于单独的检测层。 ## 兼容性 | 运行时 | v0.1 | | -------------------------------------------------- | --------------------------------- | | Node ≥ 22 LTS | CI 矩阵覆盖 22 + 24 | | Browsers | 通过标准 Web API 支持 (不在 CI 中) | | Edge runtimes | 通过标准接口支持 (不在 CI 中) | | 其他 JS 运行时 | 推迟纳入 CI | ## 许可证 [MIT](./LICENSE)
标签:ANSI转义序列过滤, API密钥检测, CISA项目, DLL 劫持, DNS 反向解析, ESM, GNU通用公共许可证, IP 地址批量处理, MITM代理, NFKC规范化, Node.js, Prompt注入, RAG安全, Red Canary, Unicode安全, Zalgo文本过滤, 凭证脱敏, 包管理器pnpm, 同形字防御, 大语言模型, 工具输出过滤, 推理标签剥离, 提示词注入防御, 数据清洗, 文本净化, 流式处理, 确定性过滤, 网络安全, 自动化攻击, 输入验证, 隐私保护, 零依赖, 零宽字符过滤