我用 Vibe Coding 写了个安全扫描器,结果它自己就是个漏洞
我用 Vibe Coding 写了个安全扫描器,结果它自己就是个漏洞
前言
"你只要跟 AI 说清楚需求,它就能帮你写出来。"这句话我信了。
上个月我心血来潮,想用 Vibe Coding 的方式搞一个内部安全扫描器。需求很简单:扫描公司内网的 Web 应用,检测常见的 OWASP Top 10 漏洞。我用 Claude Code 从零开始,只靠自然语言描述需求,一行代码没手写。
三天后,扫描器跑起来了。功能齐全,UI 漂亮,我还在团队群里秀了一把。
一周后,安全团队的一位同事看了一眼源码,对我说了一句话:"你这个扫描器,它自己就是个安全漏洞。"
这篇文章记录了我在 Vibe Coding 过程中踩过的安全坑。不是说 Vibe Coding 不好用,而是你必须知道它容易在哪些地方翻车——尤其是写安全工具的时候。
坑一:用户输入直接拼接——最基础的注入漏洞
我让 AI 写了一个目标输入框,用户输入 URL 后开始扫描。AI 生成的代码长这样:
@app.post("/scan")
async def start_scan(target: str):
# AI 生成的代码(有漏洞)
cmd = f"nmap -sV -sC {target}"
result = subprocess.run(cmd, shell=True, capture_output=True, text=True)
return {"result": result.stdout}
看出来问题了吗?
shell=True 加上字符串直接拼接,这是经典的命令注入漏洞。如果用户输入的是 example.com; rm -rf /,那 nmap -sV -sC example.com; rm -rf / 就会被执行。
正确的写法:
import shlex
import subprocess
import re
from urllib.parse import urlparse
def validate_target(target: str) -> str:
"""严格校验目标地址"""
# 1. 解析 URL
parsed = urlparse(target if '://' in target else f'http://{target}')
hostname = parsed.hostname
if not hostname:
raise ValueError("无效的目标地址")
# 2. 只允许域名和 IP,禁止特殊字符
if not re.match(r'^[a-zA-Z0-9.-]+$', hostname):
raise ValueError("目标地址包含非法字符")
# 3. 禁止扫描内网地址(安全扫描器的基本素养)
import ipaddress
try:
ip = ipaddress.ip_address(hostname)
if ip.is_private:
raise ValueError("禁止扫描内网地址")
except ValueError:
pass # 不是 IP 地址,是域名,继续
return hostname
@app.post("/scan")
async def start_scan(target: str):
clean_target = validate_target(target)
# 使用列表形式传参,避免 shell 注入
result = subprocess.run(
["nmap", "-sV", "-sC", clean_target],
capture_output=True, text=True, timeout=300
)
return {"result": result.stdout}
这个漏洞,如果你让 AI 帮你 review 自己的代码,它大概率能发现。但如果你不主动问,AI 生成的代码默认就是最"方便"的写法——方便往往意味着不安全。
坑二:硬编码的密钥和凭证
扫描器需要调用几个外部漏洞库的 API 来查询 CVE 信息。AI 生成的代码里,API Key 是这样写的:
# AI 生成的代码(有漏洞)
VULNDB_API_KEY = "vulndb_sk_a1b2c3d4e5f6g7h8i9j0"
SHODAN_API_KEY = "aBcDeFgHiJkLmNoPqRsT"
NVD_API_KEY = "nvdb-12345-67890-abcdef"
async def query_cve(cve_id: str):
headers = {"Authorization": f"Bearer {VULNDB_API_KEY}"}
async with httpx.AsyncClient() as client:
resp = await client.get(f"https://vulndb.example.com/api/cve/{cve_id}", headers=headers)
return resp.json()
硬编码密钥是老生常谈的问题了,但 Vibe Coding 生成的代码里,这个问题特别普遍。因为 AI 的目标是让代码"能跑起来",硬编码是最直接的方式。
解决方案:用环境变量 + .env 文件,并且在 .gitignore 中排除。
import os
from pathlib import Path
from dotenv import load_dotenv
# 加载 .env 文件
load_dotenv(Path(__file__).parent / '.env')
VULNDB_API_KEY = os.environ.get("VULNDB_API_KEY")
SHODAN_API_KEY = os.environ.get("SHODAN_API_KEY")
if not VULNDB_API_KEY:
raise RuntimeError("VULNDB_API_KEY 环境变量未设置")
# .env 文件(不提交到 git)
VULNDB_API_KEY=your_key_here
SHODAN_API_KEY=your_key_here
额外提醒:如果你用 Git 管理代码,即使后来把密钥删了,它还是存在于 git 历史里。需要用 git filter-branch 或 BFG Repo Cleaner 清理历史。
坑三:错误处理泄露敏感信息
扫描器的异常处理,AI 生成的是这样的:
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
return JSONResponse(
status_code=500,
content={
"error": str(exc),
"type": type(exc).__name__,
"traceback": traceback.format_exc() # AI 很"贴心"地加了完整堆栈
}
)
在开发环境这很方便调试,但在生产环境,完整的 traceback 会暴露你的代码结构、文件路径、数据库连接信息甚至密钥。
正确的做法:
import logging
logger = logging.getLogger(__name__)
@app.exception_handler(Exception)
async def global_exception_handler(request, exc):
# 记录完整错误到日志(不暴露给用户)
logger.error(f"Unhandled exception: {exc}", exc_info=True, extra={
"request_path": request.url.path,
"request_method": request.method,
})
# 返回给用户的只有通用错误信息
return JSONResponse(
status_code=500,
content={
"error": "服务器内部错误,请稍后重试",
"request_id": str(uuid.uuid4()) # 用于问题追踪
}
)
坑四:扫描结果未脱敏
扫描器会检测目标应用的漏洞,结果页面直接展示了所有发现。问题是,有些漏洞的 payload 本身包含敏感信息——比如 SQL 注入检测时获取到的数据库版本、表名、甚至部分数据。
# AI 生成的代码(有漏洞)—— 直接展示所有结果
@app.get("/scan/{scan_id}/results")
async def get_results(scan_id: str):
results = await db.get_scan_results(scan_id)
return {"results": results} # 原封不动返回所有数据
如果你把扫描结果分享给开发团队或者外部合作方,可能会无意中泄露敏感的漏洞详情。
import re
def sanitize_results(results: list[dict]) -> list[dict]:
"""脱敏扫描结果"""
sanitized = []
for item in results:
clean_item = {
"vuln_type": item["vuln_type"],
"severity": item["severity"],
"url": _mask_url(item["url"]),
"description": item["description"],
"recommendation": item.get("recommendation", ""),
# 不返回原始 payload 和详细证据
"evidence": "[已脱敏]" if item.get("evidence") else None,
}
sanitized.append(clean_item)
return sanitized
def _mask_url(url: str) -> str:
"""遮蔽 URL 中的敏感参数"""
# 把查询参数中的值替换成 ***
return re.sub(r'([?&][^=]+=)[^&]*', r'\1***', url)
坑五:权限控制形同虚设
Vibe Coding 最容易忽略的就是权限控制。AI 生成的代码通常假设"所有用户都是管理员",因为这样逻辑最简单。
# AI 生成的代码(有漏洞)—— 任何人都能执行任何操作
@app.post("/scan")
async def start_scan(target: str):
# 没有任何权限检查
...
@app.delete("/scan/{scan_id}")
async def delete_scan(scan_id: str):
# 任何人都能删除任何扫描记录
await db.delete_scan(scan_id)
即使是内部工具,也应该有基本的权限控制。至少要区分"谁能发起扫描"和"谁能查看结果"。
from fastapi import Depends, HTTPException
from fastapi.security import HTTPBearer
security = HTTPBearer()
async def require_scanner_role(token = Depends(security)):
"""校验用户是否有扫描权限"""
user = await verify_token(token.credentials)
if "scanner" not in user.get("roles", []):
raise HTTPException(status_code=403, detail="没有扫描权限")
return user
@app.post("/scan")
async def start_scan(target: str, user = Depends(require_scanner_role)):
logger.info(f"用户 {user['name']} 发起扫描: {target}")
...
反思:Vibe Coding 的安全边界
经过这次踩坑,我总结了几条 Vibe Coding 的安全原则:
1. 不要相信 AI 生成的安全相关代码。AI 的训练目标是"生成正确的代码",而不是"生成安全的代码"。安全是一个额外的约束,AI 默认不会考虑。
2. 每次让 AI 生成代码后,自己 review 一遍。重点关注:用户输入处理、密钥管理、错误处理、权限控制这四个方面。
3. 用安全工具扫描 AI 生成的代码。我后来养成习惯,每次 AI 生成代码后,都用 bandit(Python 安全扫描工具)跑一遍:
bandit -r src/ -ll -ii --skip B101
# -ll: 只显示中高风险
# -ii: 显示具体问题
# --skip B101: 跳过 assert 警告(开发阶段需要)
4. 不要 Vibe Coding 安全核心逻辑。认证、授权、加密、密钥管理这些核心安全逻辑,必须由安全工程师手写并 review。Vibe Coding 适合写业务逻辑、UI、数据处理这些非安全核心的部分。
Vibe Coding 是个好工具,但安全这事,还真不能全靠"Vibe"。
你在 Vibe Coding 过程中踩过哪些安全坑?评论区聊聊,互相避坑。
关注「安全值班室」公众号
每天AI安全早报 + 实战攻防案例 + 网安学习路线连载
浙公网安备 33010602011771号