一. 介绍
SafetyLayer 提供六大能力:长度截断、凭据扫描/替换、策略执行(Block/Sanitize)、prompt-injection 清洗、入站凭据预检、工具输出/外部内容的 XML 包裹与解包,并通过四个 getter 暴露底层组件以便做精细控制。
二. 类型
1. SafetyLayer
pub struct SafetyLayer {
sanitizer: Sanitizer,
validator: Validator,
policy: Policy,
leak_detector: LeakDetector,
config: SafetyConfig,
}
# `SafetyLayer` 字段说明
`SafetyLayer` 是一个“门面”结构体,把四个互相独立的安全组件聚到一起,再用一份配置约束它们的运行时行为。它本身不做任何检测,所有工作都委派给内部成员。
## `sanitizer: Sanitizer`
负责 **prompt-injection 检测与清洗**。定义在 `crates/ironclaw_safety/src/sanitizer.rs`,对应类型导出包括 `SanitizedOutput` 与 `InjectionWarning`。
在 `SafetyLayer::sanitize_tool_output` 里,它的作用是:当 `injection_check_enabled = true` 或策略要求 `Sanitize` 时,对工具输出文本跑 `sanitize(&content)`,产出净化后的文本与一组 `InjectionWarning`(含 `pattern`、严重级别、命中区间、描述)。注意:注入检测这一步只在 `sanitize_tool_output` 里发生,`scan_inbound_for_secrets` / `validate_input` / `check_policy` 都不会调用它——`sanitizer` 字段是“工具输出回流 LLM 之前的最后一关”。
## `validator: Validator`
负责 **输入校验**。定义在 `crates/ironclaw_safety/src/validator.rs`,对应导出 `ValidationResult` / `ValidationError` / `ValidationErrorCode`。
在 `SafetyLayer::validate_input` 里被直接委派调用(`self.validator.validate(input)`)。它的职责更偏“结构性检查”——例如对入站文本的形状、字段、长度是否合规做断言式校验,区别于 `sanitizer` 的语义级注入检测。注意 `sanitize_tool_output` 不调用 `validator`,二者是不同维度:一个是“内容是否能送进流程”,一个是“内容本身是否安全”。
## `policy: Policy`
负责 **安全策略决策**。定义在 `crates/ironclaw_safety/src/policy.rs`,对应导出 `Policy`、`PolicyRule`、`PolicyAction`(`Allow`/`Sanitize`/`Block`)、`Severity`。
- `check_policy(&content)` 把 `Policy::check` 的结果直接返回给调用方做只读查询。
- `sanitize_tool_output` 里 `Policy::check` 命中 `PolicyAction::Block` 会让整段输出被替换成 `[Output blocked by safety policy]`;命中 `Sanitize` 则会强制走一次 `Sanitizer`。
`Policy::default()` 在 `new` 里被装载,意思是策略集是编译期默认 + 运行时可通过 `policy()` getter 替换扩展(设计上是只读引用暴露)。
## `leak_detector: LeakDetector`
负责 **凭据/敏感信息泄漏检测**。定义在 `crates/ironclaw_safety/src/leak_detector.rs`,对应导出 `LeakDetector`、`LeakPattern`、`LeakMatch`、`LeakAction`、`LeakSeverity`、`LeakScanResult`、`LeakDetectionError`。
它是 `SafetyLayer` 中被调用最多的字段,三个方法都用它:
1. `sanitize_tool_output`:`scan_and_clean` 做“扫描 + 原地替换”,命中错误会阻断整段输出。
2. `scan_inbound_for_secrets`:同样用 `scan_and_clean`,命中就给用户发固定的 “message appears to contain a secret …” 警告,阻止进入 LLM。
3. 通过 `leak_detector()` getter 暴露给 bridge 层,用于那些绕过 `sanitize_tool_output` 但仍会进入 SSE 订阅者的事件(如 `CodeExecuted`)做 verbose-only 脱敏。
## `config: SafetyConfig`
定义在 [lib.rs:65-69](/E:/codes/Rust/projects/ironclaw/crates/ironclaw_safety/src/lib.rs),只有两个字段:
```rust
pub struct SafetyConfig {
pub max_output_length: usize,
pub injection_check_enabled: bool,
}
```
- `max_output_length`:工具输出长度上限。`sanitize_tool_output` 在内容超长时按 `is_char_boundary` 安全截断,避免把多字节 UTF-8 字符从中间切坏,然后追加 `[... truncated: showing X/Y bytes ...]` 提示,并发一条 `Severity::Low` 的 `output_too_large` 告警。
- `injection_check_enabled`:是否启用 prompt-injection 扫描。`true` 时 `sanitize_tool_output` 会无条件再走一次 `Sanitizer`;`false` 时只有策略命中 `Sanitize` 才走。
注意 `SafetyConfig` 只控制上面这两个开关,**不**包含凭据规则集或策略规则集——这些分别固化在 `LeakDetector` 内部和 `Policy::default()` 里。
# 五者关系一句话总结
`SafetyLayer` 是一个组合器:`leak_detector` 抓凭据、`policy` 决定放行/净化/阻断、`sanitizer` 做语义级注入清洗、`validator` 做结构校验、`config` 决定「多长算长、洗不洗」。调用方拿到 `SanitizedOutput` / `ValidationResult` / `Vec<&PolicyRule>` / `Option<String>` 这几种结果型,再决定是阻断、回写还是仅告警。
需要继续看 `LeakDetector` 的规则来源或 `Policy` 的默认规则集吗?告诉我哪个,我接着拉。2. Policy
①策略行为
pub enum PolicyAction {
/// Log a warning but allow.
Warn,
/// Block the content entirely.
Block,整段替换成 [Output blocked by safety policy],原内容丢弃
/// Require human review.
Review,
/// Sanitize and continue.
Sanitize,内容被 Sanitizer::sanitize 清洗后继续往下走
}pub struct Policy {
rules: Vec<PolicyRule>,
}pub struct PolicyRule {
/// Rule identifier.
pub id: String,
/// Human-readable description.
pub description: String,
/// Severity if violated.
pub severity: Severity,
/// The pattern to match (regex).
pattern: Regex,
/// Action to take when violated.
pub action: PolicyAction,
}
/// Severity level for safety issues.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Severity {//不同策略界别
Low,
Medium,
High,
Critical,
}3. Sanitizer(清洗)
/// Sanitizer for external data.
pub struct Sanitizer {
/// Fast pattern matcher for known injection patterns.
pattern_matcher: AhoCorasick,
/// Patterns with their metadata.
patterns: Vec<PatternInfo>,
/// Regex patterns for more complex detection.
regex_patterns: Vec<RegexPattern>,
}• # Sanitizer::new() 检测的危险情况
默认装载 17 条 Aho-Corasick 关键词 + 4 条正则,按攻击意图分四类。
## 1. 直接指令覆盖(让模型“忘记/忽略”系统提示)
关键词 严重级别 触发语义
━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━
ignore all previous Critical 全量覆盖前文指令
───────────────────── ────────── ──────────────────
ignore previous High 部分覆盖前文指令
───────────────────── ────────── ──────────────────
forget everything High 重置上下文
───────────────────── ────────── ──────────────────
disregard Medium 潜在指令覆盖
## 2. 角色操控(让模型“变成别的”)
关键词 严重级别 触发语义
━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━
you are now High 改写助手身份
─────────────── ────────── ──────────────
act as Medium 潜在角色切换
─────────────── ────────── ──────────────
pretend to be Medium 潜在角色扮演
## 3. 会话结构注入(伪造消息段或系统消息)
关键词 严重级别 触发语义
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━
system: Critical 伪造 system 消息
───────────────────────────────────────── ────────── ──────────────────
assistant: High 伪造助手回复
───────────────────────────────────────── ────────── ──────────────────
user: High 伪造用户消息
───────────────────────────────────────── ────────── ──────────────────
new instructions / updated instructions High 注入新指令
## 4. 特殊 token / 代码块(攻击 tokenizer 或触发命令执行)
关键词 严重级别 触发语义
━━━━━━━━━━━━━━━━━━ ━━━━━━━━━━ ━━━━━━━━━━━━━━━━━━━━
`< / >`
────────────────── ────────── ────────────────────
[INST] / [/INST] Critical 注入指令分隔 token
────────────────── ────────── ────────────────────
```system High 用代码块伪装系统段
────────────────── ────────── ────────────────────
```bash\nsudo Medium 嵌入式危险命令
正则补充 4 条:base64[:\s]+[A-Za-z0-9+/=]{50,}(中)、eval\s*\((高)、exec\s*\((高)、\x00 空字节(Critical)。
## 检测到之后怎么处理
来自 /E:/codes/Rust/projects/ironclaw/crates/ironclaw_safety/src/sanitizer.rs:
1. 用 Aho-Corasick(大小写不敏感)+ 正则双通道扫描,命中即收集 InjectionWarning。
2. 按 Severity 倒序排序,Critical 排最前。
3. 关键逻辑:has_critical = warnings.iter().any(|w| w.severity == Severity::Critical)。
4. 只要 任一 命中 Critical,整段内容走 escape_content:
- 转义 <|、|>、[INST]、[/INST];
- 删掉所有空字节 \x00;
- 行首出现 system: / user: / assistant: 时,前面加 [ESCAPED] 前缀;
- was_modified = true。
5. 没命中 Critical:内容原样保留,was_modified = false,只把告警带出去。
## 一句话总结
Sanitizer 盯的是 “试图改写指令/角色/会话结构” 这一类 prompt injection;规则侧重关键词命中,且只有 Critical 级别会真正改写内容,High/Medium 仅作为告警携带——是否阻断由 Policy 的 Block 决策和
SafetyLayer::sanitize_tool_output 决定,而不是 Sanitizer 自己做。4. Validator(输入校验)
/// Input validator.
pub struct Validator {
/// Maximum input length.
max_length: usize,
/// Minimum input length.
min_length: usize,
/// Forbidden substrings.
forbidden_patterns: HashSet<String>,
} 参数:input 待校验文本,field 错误信息里要标注的字段名(多用于递归校验 JSON 时携带路径,如 "params.user.bio")。
## 六项检查
1. 最大长度(硬错误 TooLong)
- input.len() > self.max_length → 触发,默认 max_length = 100_000。
- 错误信息示例:"Input too long: 120000 bytes (max 100000)"。
2. 最小长度(硬错误 TooShort)
- input.len() < self.min_length → 触发,默认 min_length = 1。
- 错误信息示例:"Input too short: 0 bytes (min 1)"(但 0 在 validate 阶段就已经被拦了,所以这里实际只会因为配置上调 min_length 才命中)。
3. 空字节检测(硬错误 InvalidEncoding)
- input.chars().any(|c| c == '\x00') → 触发,标记 "Input contains null bytes"。
4. 禁用模式黑名单(硬错误 ForbiddenContent)
- 对 self.forbidden_patterns(HashSet<String>,默认由 Validator::new() 装载)做小写包含匹配。
- 命中后错误信息:"Input contains forbidden pattern: <pattern>"。
- 这条和 Sanitizer 的关键词是两条独立通道:Sanitizer 关心 prompt-injection;Validator 关心输入侧违禁词。
5. 异常空白比(软警告)
- whitespace = 空白字符数 / input.len() > 0.9 且 input.len() > 100。
- 用 with_warning 写入 "Input has unusually high whitespace ratio",不阻断。
6. 字符过度重复(软警告)
- 调 has_excessive_repetition(input),命中后写 "Input has excessive character repetition",同样不阻断。
## 返回结构
ValidationResult 由 is_valid、errors: Vec<ValidationError>、warnings: Vec<String> 组成(顶部定义)。逻辑上:
- 每条硬错误通过 result.merge(ValidationResult::error(...)) 累积,并维护 is_valid = self.is_valid && other.is_valid——所以任一条硬错误命中即整体无效。
- 软警告通过 with_warning 累加,不影响 is_valid。
- 默认从 ValidationResult::ok() 起步,因此全部通过时 is_valid = true 且 errors、warnings 都为空。
## 它跟谁联动
- Validator::validate(line 119):先 is_empty() 检查,命中即返回;否则委托到这里。
- Validator::validate_tool_params(line 195 起):递归遍历 JSON 树,对每个字符串值调用 validate_non_empty_input(s, path),从而把同样的六项检查复用到了工具参数的字符串字段上,错误 field 里带上 JSON 路
径。
# 一句话总结
validate_non_empty_input 是一次纯结构性的“输入体检”:长度上下限 + 空字节 + 禁用词黑名单是硬阻断,异常空白比和过度重复只是软警告;它不查 prompt injection(那是 Sanitizer 的活)、不查凭据(那是
LeakDetector 的活)、不查策略(那是 Policy 的活),只做“形状对不对”。5. LeakDetector (输出的密钥探测)
/// Detector for secret leaks in output data.
pub struct LeakDetector {
patterns: Vec<LeakPattern>,
/// For fast prefix matching of known patterns
prefix_matcher: Option<AhoCorasick>,
known_prefixes: Vec<(String, usize)>, // (prefix, pattern_index)
}共 **20 条**(rg 报 18 是因为每条 `LeakPattern {` 后面紧跟 `name:` 也被匹配了一次)。下面是完整清单。
# `LeakDetector::default_patterns` 列表
每条规则的字段:`name`(模式名)、`regex`(匹配模式)、`severity`(`Critical` / `High` / `Medium`)、`action`(`Block` / `Redact` / `Warn`)。整段定义在 [crates/ironclaw_safety/src/leak_detector.rs:417-650](/E:/codes/Rust/projects/ironclaw/crates/ironclaw_safety/src/leak_detector.rs)。
## 1. LLM / AI 提供方密钥(全部 Critical + Block)
| 模式名 | 正则 | 说明 |
|---|---|---|
| `openai_api_key` | `sk-(?:proj-)?[a-zA-Z0-9]{20,}(?:T3BlbkFJ[a-zA-Z0-9_-]*)?` | OpenAI 经典 + 项目前缀,支持新旧两种格式 |
| `anthropic_api_key` | `sk-ant-api[a-zA-Z0-9_-]{90,}` | Anthropic 管理 API key |
| `anthropic_oauth_token` | `\bsk-ant-oat\d{2}-[a-zA-Z0-9_-]{50,}` | Anthropic OAuth token |
| `openrouter_api_key` | `\bsk-or-v1-[a-fA-F0-9]{40,}` | OpenRouter |
| `groq_api_key` | `\bgsk_[A-Za-z0-9]{30,}` | Groq |
| `nearai_session` | `sess_[a-zA-Z0-9]{32,}` | NEAR AI 会话 token |
## 2. 云与 SaaS(全部 Critical/High + Block)
| 模式名 | 正则 | 严重级别 |
|---|---|---|
| `aws_access_key` | `AKIA[0-9A-Z]{16}` | Critical |
| `google_api_key` | `AIza[0-9A-Za-z_-]{35}` | High |
| `github_token` | `gh[pousr]_[A-Za-z0-9_]{36,}` | Critical(覆盖 classic PAT/OAuth/user-to-user/server-to-server) |
| `github_fine_grained_pat` | `github_pat_[a-zA-Z0-9]{22}_[a-zA-Z0-9]{59}` | Critical |
| `stripe_api_key` | `sk_(?:live|test)_[a-zA-Z0-9]{24,}` | Critical |
| `slack_token` | `xox[baprs]-[0-9a-zA-Z-]{10,}` | High(bot/app/user/replay/stitch) |
| `twilio_api_key` | `SK[a-fA-F0-9]{32}` | High |
| `sendgrid_api_key` | `SG\.[a-zA-Z0-9_-]{22}\.[a-zA-Z0-9_-]{43}` | High |
| `telegram_bot_token` | `\b\d{8,12}:AA[A-Za-z0-9_-]{30,}\b` | Critical(词边界,避免时间戳误报) |
## 3. 私钥文件头(全部 Critical + Block)
| 模式名 | 正则 |
|---|---|
| `pem_private_key` | `-----BEGIN\s+(?:RSA\s+)?PRIVATE\s+KEY-----` |
| `ssh_private_key` | `-----BEGIN\s+(?:OPENSSH|EC|DSA)\s+PRIVATE\s+KEY-----` |
## 4. 通用 HTTP 鉴权头(High + **Redact** 而非 Block)
| 模式名 | 正则 | 备注 |
|---|---|---|
| `bearer_token` | `Bearer\s+[a-zA-Z0-9._-]{20,}` | 注释明说“可能是有意携带”,所以选 redact |
| `auth_header` | `(?i)authorization:\s*[a-zA-Z]+\s+[a-zA-Z0-9_-]{20,}` | 同上,做脱敏而非阻断 |
## 5. 高熵兜底(Medium + **Warn**,不阻断)
| 模式名 | 正则 | 备注 |
|---|---|---|
| `high_entropy_hex` | `\b[a-fA-F0-9]{64}\b` | 命中 64 位十六进制(典型 SHA256 长度),仅警告。注释说明:regex crate 不支持 look-around,所以用词边界;可能误报 hash |
# 几个值得注意的设计点
- **`Block` vs `Redact` vs `Warn` 的差别**(这跟前面讲的 `PolicyAction` 是平行的另一套):
- `Block`(17 条)→ 在 `SafetyLayer::sanitize_tool_output` 里会把整段替换为 `[Output blocked due to potential secret leakage]`。
- `Redact`(2 条 bearer 类)→ 仅把命中片段替换为占位符,**不阻断**整体内容。
- `Warn`(1 条高熵 hex)→ 仅产生 `LeakMatch`,由调用方决定要不要再处理。
- **加 `T3BlbkFJ` 后缀的 OpenAI key**:保留了对老格式项目 key(`sk-...T3BlbkFJ...`)的兼容。
- **`\b` 词边界**:用在 `openrouter`、`anthropic_oauth_token`、`telegram_bot_token`、`groq`、`high_entropy_hex` 上,避免在长字符串中间误命中(例如日志时间戳里的连续数字串)。
- **覆盖范围刻意偏向“已知高价值 token”**:Lumen/Critical 多为按 token 前缀识别,因此对未登记的新格式会有遗漏;这就是为什么 `LeakDetector` 还保留了 `credential_detect.rs` 中的手动检测模块做补充。
如果需要,我下一步可以把 `credential_detect` 模块里 `http_parts_contain_manual_credentials` / `params_contain_manual_credentials` 那两条手动补充规则也拉出来对照。