DEV Community

韩

Posted on

你的 AI Agent 其实是个定时炸弹:5 个你没意识到的安全漏洞

1. 沙箱隔离:别让 Agent 拿着服务器的钥匙到处跑

最常见的错误:给 Agent 分配和开发者账号一样的权限。Agent 需要读文件、执行 Shell、调用 API → 它就自动继承了你所有的 IAM 角色。一次 Agent 被攻破 = 整个基础设施沦陷。

# ❌ 危险示范:Agent 拥有全部权限
tools = [
    FileReadTool(),           # 可以读取任何文件,包括 .env
    ShellTool(),              # 可以执行任何 shell 命令
    APIClientTool(api_key=os.environ["SECRET_KEY"])  # 密钥直接暴露给 LLM
]

# ✅ 安全方案:工具级权限控制 + 审计日志
from goclaw import Agent, ToolPolicy

policy = ToolPolicy()
# 文件访问:只读 + 仅限项目目录
policy.add_rule("file_read",
    allowed_paths=["/app/src/", "/app/config/"],
    denied_paths=["/app/.env", "/app/secrets/"],
    audit=True
)
# Shell:禁止危险命令
policy.add_rule("shell",
    allowed_commands=["git", "npm", "pytest", "docker-compose"],
    denied_patterns=["rm -rf", "curl.*|nc ", "ssh ", "chmod 777"],
    audit=True
)
# API 密钥:通过密钥管理器引用,LLM 永远看不到真实密钥
policy.add_rule("api_call",
    require_secret_manager=True,  # 从 Vault/SSM 解析,不暴露给 LLM
    allowed_endpoints=["api.stripe.com", "api.github.com"],
    audit=True
)

agent = Agent(
    model="claude-sonnet-4",
    tools=[FileReadTool(policy=policy), ShellTool(policy=policy)],
    isolation="gvisor",  # 在 gVisor 沙箱中运行,与宿主机内核隔离
)
result = agent.run("部署最新版本")
# 每次工具调用都有日志:谁、做了什么、用了什么参数、返回了什么
Enter fullscreen mode Exit fullscreen mode

为什么这个漏洞这么普遍? 教程只教你怎么让 Agent "能干活",不教你怎么让它"安全干活"。Vercel 事件之所以发生,就是因为一个 Agent 拥有了它根本不需要的环境变量访问权限。

数据支撑:GitHub 上的 goclaw(2,901★)专门为了这个原因做了多租户隔离和5层安全防护。


2. 工具投毒:别信任 Agent 调用工具后返回的数据

AI Agent 调用工具,工具返回数据——但如果工具返回的数据被污染,用于操控 Agent 下一个决策呢?

这就是工具投毒(Tool Poisoning),比想象中常见。第三方 MCP 服务器、外部 API、甚至你自己的向量数据库,都可能返回经过精心构造的内容来影响 Agent 行为。

# ❌ 易受攻击:工具输出直接喂给 Agent
def search_codebase(query: str) -> str:
    results = vector_db.similarity_search(query, k=10)
    # 攻击者可能在向量数据库中植入提示词注入载荷
    return "\n".join([r.content for r in results])
    # 这些内容会被嵌入下一个 LLM 提示词,没有任何过滤

# ✅ 安全方案:输出净化 + Schema 验证 + 注入检测
import re

def sanitize_tool_output(raw_output: str, max_length: int = 8000) -> str:
    """移除工具输出中的潜在提示词注入模式。"""
    injection_patterns = [
        r"<\|system\|.*?\|>",      # 角色混淆
        r'<script[^>]*>.*?</script>', # XSS 向量
        r'^IGNORE ALL PREVIOUS.*',     # 直接覆盖尝试
        r'^You are now.*?:',           # 角色重分配
        r'\n(?:system|assistant|user):',  # 轮次混淆
    ]

    cleaned = raw_output[:max_length]
    for pattern in injection_patterns:
        cleaned = re.sub(pattern, "[已过滤]", cleaned, flags=re.IGNORECASE | re.DOTALL)

    if cleaned.count("[已过滤]") > 3:
        logger.warning(f"工具输出中检测到疑似注入攻击:{cleaned[:200]}")

    return cleaned

def safe_search(query: str) -> str:
    results = vector_db.similarity_search(query, k=10)
    raw = "\n".join([r.content for r in results])
    return sanitize_tool_output(raw)

agent.register_tool(
    "search_codebase",
    handler=safe_search,
    output_validator=sanitize_tool_output,
    rate_limit={"max_calls_per_minute": 30},
    requires_confirmation=True  # 可疑查询标记为需要人工审核
)
Enter fullscreen mode Exit fullscreen mode

为什么重要:Lovable 安全事件中,安全研究人员发现被操纵的上下文可以将 Agent 操作重定向到非预期目标。如果你的检索器返回污染内容,本质上等于把 Agent 控制权交给了攻击者。


3. 密钥管理:永远不要让 LLM 看到任何密钥

生产代码中见到最多的模式:Agent 直接持有 API 密钥。一旦你把 api_key=os.environ["OPENAI_KEY"] 传给 Agent 的工具定义,这个密钥就进入了 LLM 的上下文窗口。取决于你的服务商的日志策略,它可能被记录在审计日志、训练数据、甚至在提示词注入攻击中被窃取。

import os
from abc import abstractmethod

# ✅ 安全方案:密钥管理器模式 —— 密钥永远不接触 LLM 上下文

class SecretBackedTool:
    """需要密钥但永远不暴露给 LLM 的工具基类。"""

    def __init__(self, secret_name: str, secret_manager: str = "aws-ssm"):
        self.secret_name = secret_name
        self.secret_manager = secret_manager

    def _resolve_secret(self) -> str:
        """在运行时从密钥管理器解析。绝不存储在上下文中。"""
        if self.secret_manager == "aws-ssm":
            import boto3
            ssm = boto3.client('ssm')
            return ssm.get_parameter(Name=self.secret_name, WithDecryption=True)['Parameter']['Value']
        elif self.secret_manager == "hashicorp-vault":
            import hvac
            client = hvac.Client()
            return client.secrets.kv.v2.read_secret_version(
                path=self.secret_name
            )['data']['data']['value']
        elif self.secret_manager == "env":
            return os.environ[self.secret_name]

    @abstractmethod
    def execute(self, **kwargs):
        pass

    def __call__(self, *args, **kwargs):
        # 密钥解析在执行时发生,不在注册时发生
        resolved = self._resolve_secret()
        return self.execute(secret=resolved, **kwargs)


class StripeTool(SecretBackedTool):
    """示例:Stripe API 工具,零密钥暴露。"""

    def __init__(self):
        super().__init__(secret_name="/prod/stripe/api-key")

    def execute(self, secret: str, action: str, amount: int) -> dict:
        import stripe
        stripe.api_key = secret
        if action == "charge":
            return stripe.Charge.create(amount=amount, currency="usd", source="tok_visa")
        elif action == "refund":
            return stripe.Refund.create(charge=kwargs.get("charge_id"))
        return {"status": "ok"}


# ✅ 注册时工具定义中不包含任何密钥
agent.register_tool("stripe", StripeTool())
# LLM 看到的是:工具名、参数 Schema、返回类型
# LLM 看不到:实际的 API 密钥
Enter fullscreen mode Exit fullscreen mode

核心原则:密钥在执行时解析,不在注册时解析。LLM 上下文永远不包含原始凭证。


4. Agent 间认证:多 Agent 系统需要 mTLS

当你在运行多个协作的 AI Agent(比如:一个写代码,一个审查代码,一个部署)时,你面临一个跨 Agent 信任问题。如果没有认证,被攻陷的 Agent 可以冒充其他 Agent 执行未授权操作。

# ✅ mTLS 风格的 Agent 认证
import hashlib, hmac, time, json

class AgentAuth:
    """多 Agent 系统的轻量级双向认证。"""

    def __init__(self, agent_id: str, signing_key: str):
        self.agent_id = agent_id
        self.signing_key = signing_key.encode()

    def sign(self, payload: dict, nonce: str = None) -> dict:
        """使用 HMAC 签名跨 Agent 消息。"""
        nonce = nonce or f"{time.time_ns()}"
        data = json.dumps(payload, sort_keys=True) + nonce
        signature = hmac.new(
            self.signing_key, data.encode(), hashlib.sha256
        ).hexdigest()
        return {
            **payload,
            "_auth": {
                "agent_id": self.agent_id,
                "nonce": nonce,
                "signature": signature,
                "ts": time.time()
            }
        }

    def verify(self, signed_payload: dict) -> bool:
        """验证来自其他 Agent 的消息。"""
        auth = signed_payload.get("_auth", {})
        if not auth:
            return False
        # 拒绝过期消息(5分钟窗口)
        if abs(time.time() - auth.get("ts", 0)) > 300:
            return False
        payload = {k: v for k, v in signed_payload.items() if k != "_auth"}
        expected_sig = self.sign(payload, nonce=auth["nonce"])["_auth"]["signature"]
        return hmac.compare_digest(expected_sig, auth["signature"])


# Agent A(代码编写者)向 Agent B(审查者)进行身份认证
auth_a = AgentAuth("code-writer", signing_key="shared-secret-xyz")
message = auth_a.sign({
    "action": "review_code",
    "file": "/app/src/deploy.py",
    "commit": "a3f8c2d"
})

# Agent B 在接受任务前先验证
auth_b = AgentAuth("code-reviewer", signing_key="shared-secret-xyz")
if not auth_b.verify(message):
    raise PermissionError(
        f"Agent {message['_auth']['agent_id']} 身份验证失败"
    )
# 这防止了:攻击者攻陷一个 Agent 后冒充代码编写者
Enter fullscreen mode Exit fullscreen mode

现实背景:Anthropic 在2026年4月23日发布的 Claude Code 质量问题事后分析报告中指出,多 Agent 编排缺乏适当认证是导致不可预测行为的关键因素之一。当 Agent 可以互相冒充时,你根本做不到问责。


5. 审计日志:你修复不了看不见的东西

每个生产级 AI Agent 部署都需要全面、防篡改的审计日志。不是简单记录"Agent 做了什么",而是记录"确切的提示词是什么、调用了哪些工具、工具返回了什么、最终决策是什么"。

import json, hashlib
from datetime import datetime
from pathlib import Path

class AgentAuditLogger:
    """AI Agent 操作的防篡改审计日志。"""

    def __init__(self, log_path: str = "/var/log/agent-audit.jsonl"):
        self.log_path = Path(log_path)

    def log(self, event_type: str, data: dict, context: dict):
        """写入不可变审计条目。"""
        entry = {
            "ts": datetime.utcnow().isoformat() + "Z",
            "type": event_type,
            "data_hash": hashlib.sha256(
                json.dumps(data, sort_keys=True).encode()
            ).hexdigest(),
            "data": data,
            "context": {
                "model": context.get("model"),
                "session_id": context.get("session_id"),
                "user_id": context.get("user_id"),
            },
            "prev_hash": self._last_hash(),
        }
        # 追加写入,不可修改
        with open(self.log_path, "a") as f:
            f.write(json.dumps(entry) + "\n")

    def _last_hash(self) -> str:
        """获取最后一条记录的哈希值,用于链式完整性校验。"""
        if not self.log_path.exists():
            return "genesis"
        with open(self.log_path) as f:
            lines = f.readlines()
        if not lines:
            return "genesis"
        return json.loads(lines[-1])["data_hash"]

    def get_session_log(self, session_id: str):
        """获取某个会话的完整审计轨迹(用于事后分析)。"""
        events = []
        with open(self.log_path) as f:
            for line in f:
                entry = json.loads(line)
                if entry.get("context", {}).get("session_id") == session_id:
                    events.append(entry)
        return events

    def detect_anomalies(self, session_id: str):
        """检测会话中的可疑模式。"""
        events = self.get_session_log(session_id)
        anomalies = []
        for e in events:
            # 检测高频工具调用(可能的循环/DoS)
            if e["type"] == "tool_call":
                window = [
                    x for x in events
                    if abs(float(x.get("ts", 0).replace("Z", "").replace("T", " ")) -
                           float(e.get("ts", 0).replace("Z", "").replace("T", " "))) < 60
                ]
                if len(window) > 20:
                    anomalies.append(f"高频调用:60秒内{len(window)}")
            # 检测密钥访问模式
            if e["type"] == "tool_output" and "key" in str(e["data"]).lower():
                anomalies.append("检测到疑似密钥访问")
        return anomalies

# 在 Agent 中使用
audit = AgentAuditLogger()
agent = Agent(
    model="claude-sonnet-4",
    tools=[...],
    callbacks={
        "on_tool_call": lambda tool, params: audit.log("tool_call", {"tool": tool, "params": params}, ctx),
        "on_tool_output": lambda tool, output: audit.log(
            "tool_output", 
            {"tool": tool, "output_hash": hashlib.sha256(str(output).encode()).hexdigest()}, 
            ctx
        ),
        "on_decision": lambda decision: audit.log("decision", {"decision": decision}, ctx),
    }
)
Enter fullscreen mode Exit fullscreen mode

行业终于开始重视了

2026年4月的 Vercel 和 Lovable 安全事件是警钟。但即使在这些事件之前,社区已经在讨论:

多 Agent 安全问题不是理论上的。OpenHands(71,915★)和 MetaGPT(67,368★)是 GitHub 上 Star 数最高的 AI 项目之二——但它们默认都没有生产级安全防护。


今天就能做的5件事

  1. 审计你当前 Agent 的工具权限——有多少工具拥有不受限制的权限?
  2. 给所有返回外部/用户生成内容的工具加上输出净化
  3. 把密钥迁移到管理器(AWS SSM、Vault 或简单的引用方案)
  4. 如果运行多个 Agent,立即实现跨 Agent 认证
  5. 从现在开始记录所有操作——你无法修复看不见的漏洞

"原型 Agent"和"生产级 Agent"之间的差距,主要就是安全差距。工具已存在,方案已验证。现在要做的就是抢在下一个安全事件之前把它们用上。


你在生产环境中遇到过 AI Agent 安全问题吗?欢迎在评论区分享你的经历和解决方案。

相关阅读:

Top comments (0)