一、为什么工具层是安全主战场
Prompt注入只是入口,工具调用才是攻击落地的最后一公里。攻击者费尽心机注入指令,最终目标都是让Agent去调用某个危险工具:删数据、发邮件、转账、执行命令。很多团队在提示词层面严防死守,工具层却"裸奔"——Agent能调什么、参数合不合法、调用有没有记录,一概不管。本文从工具层构建四道防线。
二、第一道防线:能力白名单
不是所有工具都该对Agent开放,更不该对所有人开放。用工具注册表 + 权限装饰器实现最小权限:
class ToolRegistry:
"""集中管理工具注册与权限"""
def __init__(self):
self._tools = {} # name -> Tool
self._perms = {} # name -> set(permission)
def register(self, name, permission="user", description="", schema=None):
def decorator(fn):
self._tools[name] = {
"fn": fn, "permission": permission,
"description": description, "schema": schema or {},
}
return fn
return decorator
def check_permission(self, name, user_roles: set) -> bool:
"""校验调用者角色是否满足工具所需权限"""
tool = self._tools.get(name)
if not tool:
return False
required = tool["permission"]
# admin 拥有全部权限;user 仅能调用 user 级工具
return required == "user" or "admin" in user_roles
registry = ToolRegistry()
@registry.register("read_file", permission="user", description="读取允许范围内的文件")
def read_file(path: str) -> str:
return safe_read(path)
@registry.register("delete_record", permission="admin", description="删除数据库记录")
def delete_record(table: str, record_id: int) -> str:
return db_delete(table, record_id)
def invoke_tool(name, args, user_roles):
if not registry.check_permission(name, user_roles):
return {"error": f"权限不足:工具 {name} 需要更高权限"}
return registry._tools[name]["fn"](**args)
权限粒度建议:user(普通用户可用)、admin(管理员)、internal(仅系统内部调用,不暴露给模型自主选择)。Agent的system prompt里只放它当前角色能用的工具清单,看不到的就不会调用。
三、第二道防线:参数校验
大模型生成的参数经常"越界":路径穿越、超长输入、非法枚举值。进执行器之前必须过schema校验:
import json
def validate_args(tool_name: str, args: dict, schema: dict) -> tuple:
"""按JSON Schema校验工具参数,返回(是否通过, 错误信息)"""
if not schema:
return True, ""
required = schema.get("required", [])
for field in required:
if field not in args or args[field] in (None, ""):
return False, f"缺少必填参数: {field}"
properties = schema.get("properties", {})
for field, value in args.items():
spec = properties.get(field, {})
vtype = spec.get("type", "string")
if vtype == "string" and not isinstance(value, str):
return False, f"参数 {field} 应为字符串"
if vtype == "integer" and not isinstance(value, int):
return False, f"参数 {field} 应为整数"
if vtype == "array" and not isinstance(value, list):
return False, f"参数 {field} 应为数组"
# 路径类参数防穿越
if field in ("path", "file") and isinstance(value, str):
if ".." in value or value.startswith("/"):
return False, f"参数 {field} 包含非法路径"
# 枚举约束
if "enum" in spec and value not in spec["enum"]:
return False, f"参数 {field} 不在允许范围内: {spec['enum']}"
# 长度上限
if "maxLength" in spec and isinstance(value, str) and len(value) > spec["maxLength"]:
return False, f"参数 {field} 超长"
return True, ""
def safe_invoke(name, args, user_roles):
tool = registry._tools.get(name)
if not tool:
return {"error": "未知工具"}
ok, msg = validate_args(name, args, tool["schema"])
if not ok:
log_security_event("arg_validation_failed", name=name, reason=msg)
return {"error": msg}
return invoke_tool(name, args, user_roles)
校验失败不仅要拒绝,还要记录安全事件——频繁的参数校验失败往往意味着攻击者在试探工具边界。
四、第三道防线:敏感操作审批门
高危工具(删除、转账、发信、改配置)不能由模型单方面触发,必须挂人工审批门:
HIGH_RISK_TOOLS = {"delete_record", "transfer_money", "send_email", "modify_config"}
def invoke_with_approval(name, args, user_roles, approver_callback):
"""高危工具:先挂起,等待人工审批"""
if name in HIGH_RISK_TOOLS:
request_id = uuid.uuid4().hex[:12]
# 生成审批请求,推送到审批人(企业微信/邮件/管理后台)
approver_callback(request_id, name, args)
return {"status": "pending_approval",
"request_id": request_id,
"message": f"操作 {name} 需要人工审批,审批通过后自动执行"}
return safe_invoke(name, args, user_roles)
def approve_and_execute(request_id, name, args, user_roles, decision=True):
"""审批回调:通过才真正执行"""
if not decision:
log_security_event("approval_rejected", name=name, request_id=request_id)
return {"error": "审批已拒绝"}
log_security_event("approval_passed", name=name, request_id=request_id)
return safe_invoke(name, args, user_roles)
审批门同时要设超时策略:超过5分钟未审批自动拒绝,防止恶意请求长时间挂起占用资源。
五、第四道防线:全量调用审计
所有工具调用——无论成败——必须留痕,这是事后追溯和红蓝演练的基础:
def audit_tool_call(trace_id, user_id, name, args, result, risk="low"):
"""全量审计日志:调用方、参数、结果、风险等级"""
record = {
"trace_id": trace_id, "user_id": user_id, "tool": name,
"args": json.dumps(args, ensure_ascii=False)[:500],
"result_preview": str(result)[:200],
"risk": risk, "ts": time.strftime("%Y-%m-%d %H:%M:%S"),
}
# 写入独立的审计库(与业务库分离,且只追加不修改)
audit_db.insert(record)
if risk == "high":
alert_security_team(record)
return record
审计数据的三条要求:只追加不可篡改、保留至少180天、支持按trace_id/user_id/工具名检索。定期用审计日志做异常检测——例如"同一用户在1分钟内调用删除工具5次"这类行为模式。
六、纵深防御清单
| 防线 | 拦截对象 | 关键实现 |
|---|---|---|
| 能力白名单 | 越权调用 | 工具注册表+角色权限 |
| 参数校验 | 非法/恶意参数 | JSON Schema+路径穿越检查 |
| 审批门 | 高危操作 | 人工审批+超时拒绝 |
| 调用审计 | 事后追溯 | 全量只追加日志+异常检测 |
**核心观点**:工具层安全遵循"默认拒绝"原则——没注册的工具调不了、没权限的角色调不了、参数不合规调不了、高危操作没人批调不了。配合每季度一次的红蓝演练,用真实攻击脚本打自己的Agent,比任何安全文章都管用。记住:提示词防线可以被绕过,工具层的硬约束不会。