跳到主要内容

使用 Cursor 进行 Vibe Coding 的安全思维

Vibe coding security with Cursor

用 Cursor 进行 Vibe Coding 既快速又有趣:你描述想要什么,智能体写出代码,你保持流畅的节奏,而不用停下来逐行阅读。问题在于,一个被训练成满足提示词的模型,会乐意产出能运行、看起来也很干净的代码,但它可能仍带有 SQL 注入、硬编码的 API 密钥,或者一个信任客户端的授权检查。本指南为你提供一套可重复的安全思维,用于 Cursor 中的 Vibe Coding 工作流——从识别 AI 生成代码最常产生的缺陷,到审计输出、强制执行规则、处理密钥,以及在发布前执行部署前检查清单。

什么是 Vibe Coding,为什么安全很重要​

Vibe Coding 意味着你带着意图进行引导,让模型去填充实现细节。你要一个「带会话 Cookie 的登录端点」,Cursor 就写出路由、数据库调用和中间件。产出通常能用,但能用不等于安全。模型优化的是让代码跑起来,而不是防御一个会阅读同一份代码的攻击者。

在这种工作流中,安全不是变得不那么重要,而是更重要,原因有三:

  • 你的审查更少。 当模型每个提示词写出几十行代码时,人工审查环节会成为瓶颈并被跳过。
  • 模型是从不安全的代码中学出来的。 公开仓库里有大量字符串拼接的 SQL 和客户端存储的 token,因此这些模式会出现在补全结果中。
  • 影响面很大。 Agent 模式可以一次触及许多文件,因此一个不安全模式会被复制到整个代码库中。

安全不是一个最后才附加上去的独立阶段。它是你使用 Cursor 进行提示、审查和部署时内化的一种持续习惯。

你仍然是审查者

Cursor 的智能体可以写出能编译、能通过你的测试,却仍然带有漏洞的代码。模型无法为你的风险签字背书。请把每一段生成的代码都当作一位快速但粗心的初级开发者的草稿。

AI 生成代码中最常见的漏洞​

以下这些故障模式在 AI 生成的代码中反复出现。学会辨认每一种的形态,这样你在扫读时就能发现它,而不必逐行阅读。

漏洞在生成代码中的表现模型为什么会产生它
SQL 注入用字符串拼接而非参数构建查询拼接写起来更简单,读起来也「更自然」
XSS未净化的用户输入被 innerHTML 或 dangerouslySetInnerHTML 渲染直接渲染 HTML 是达到视觉结果的最短路径
硬编码密钥 / API 密钥源码文件中留下 const API_KEY = "sk-live-..."字面量值让代码片段自包含且可运行
认证 / 会话处理被破坏Token 存储在 localStorage 中,无过期时间,无服务端校验客户端存储最容易在浏览器中演示
过于宽松的 CORSAccess-Control-Allow-Origin: * 且带凭据通配符是能立刻消除报错的一行修复
不安全的反序列化pickle.loads、eval,或直接把 JSON 当作对象信任反序列化一切很方便,且默认输入可信
暴露的管理端点诸如 /admin/users 的路由没有任何角色检查模型实现了功能,却忘了加防护
缺少输入校验请求体直接用于逻辑,没有任何 schema 检查当正常路径已经能跑通时,校验显得多余

JavaScript 中的 SQL 注入​

下面是经典的一对:一个拼接用户输入的查询,以及用参数化占位符写成的同一个查询。

// VULNERABLE: user input is concatenated straight into the query
app.get('/user', async (req, res) => {
const sql = "SELECT * FROM users WHERE email = '" + req.query.email + "'";
const rows = await db.query(sql);
res.json(rows);
});
// FIXED: parameterized query, input never becomes part of the SQL string
app.get('/user', async (req, res) => {
const sql = 'SELECT * FROM users WHERE email = $1';
const rows = await db.query(sql, [req.query.email]);
res.json(rows);
});

Python 中的授权检查​

有漏洞的版本信任客户端发送的一个标志。修复后的版本在服务端检查已认证的会话。

# VULNERABLE: the client tells the server whether it is an admin
@app.get("/admin/users")
def admin_users(request):
if request.args.get("is_admin") == "true":
return db.list_users()
return {"error": "forbidden"}
# FIXED: the server decides based on the authenticated session
@app.get("/admin/users")
def admin_users(request):
user = request.session.get("user")
if not user or user["role"] != "admin":
abort(403)
return db.list_users()

把会话 token 存在 localStorage 中会让页面上任何脚本都能读取它,从而把单个 XSS 缺陷变成完整的账号接管。

// VULNERABLE: token readable by any script, no protection against XSS
localStorage.setItem('token', response.token);
// FIXED: server sets an httpOnly cookie that scripts cannot read
res.cookie('session', token, {
httpOnly: true,
secure: true,
sameSite: 'strict',
maxAge: 1000 * 60 * 60,
});

使用 Cursor 审计 AI 生成的代码​

审计并不意味着逐行阅读。它意味着让模型再跑一遍,去攻击第一遍写出的东西。当你给 Cursor 设定正确的角色和清晰的目标时,它在这方面很擅长。

安全审查角色的提示词​

在智能体完成一个功能后,在 Agent 模式中复用这段提示词。它迫使模型从构建者切换为攻击者。

You are a security reviewer auditing code a previous agent just wrote.
For every file changed in this feature:
1. List each place that handles user input, authentication, or secrets.
2. For each, describe the concrete attack an unauthenticated user could run.
3. Show the vulnerable line and the fixed line side by side.
4. Rate severity as low, medium, high, or critical with one sentence of reasoning.
Do not rewrite the whole feature. Report findings only.

在 Agent 模式下要求进行威胁建模​

除了审查已完成的代码,你还可以让 Cursor 在动手写之前先做威胁建模。这能暴露行级审查所遗漏的设计缺陷。

Before implementing this billing endpoint, do a short threat model:
- Who can call this route and with what privileges?
- What happens if the amount, currency, or user ID is tampered with?
- Which trust boundary does this input cross?
Then implement the endpoint with those mitigations in place.

使用 .cursor/rules 强制执行检查​

你可以把审计本身编码成一条规则,让智能体在每次改动时自动运行它。.cursor/rules/ 中的规则会被加载到上下文中,这意味着安全清单在智能体工作时始终在场。

每次智能体会话之后都做审计

让「跑一遍安全审查」成为反射动作,适用于任何触及认证、输入处理或密钥的智能体会话。它只需要一个提示词,却能在这些问题进入提交之前抓住上表中的故障模式。

用 Cursor 规则强制执行安全要求​

一份项目规则文件能让你的安全期望始终呈现在模型面前,而无需反复重申。创建 .cursor/rules/security.mdc 并提交它,这样每一位协作者都会得到相同的护栏。

---
description: Security requirements for all code in this project
globs: ["**/*.ts", "**/*.tsx", "**/*.py", "**/*.js"]
alwaysApply: true
---

# Security requirements

- Never write SQL by concatenating user input. Use parameterized queries or an ORM.
- Never render user input with innerHTML or dangerouslySetInnerHTML. Escape it or use a safe renderer.
- Never hardcode secrets, API keys, or tokens. Read them from environment variables.
- Never grant administrative access based on a value supplied by the client.
- Always validate request bodies against a schema before use.
- Always set httpOnly, secure, and sameSite on session cookies.
- Never use eval, exec, or pickle.loads on untrusted input.
- When adding a new endpoint, state explicitly which role can call it.
- When touching auth or input handling, add a security-review note in the response.

设置 alwaysApply: true 后,该规则会被注入到每一次请求中,因此智能体在编写过程中会自我纠正,而不是等你事后发现问题。你还可以用 globs 把规则限定到特定文件夹,这样后端的安全规则就不会干扰前端工作。

管理密钥与环境变量​

硬编码密钥是最容易意外发布的漏洞,因为一个字面量值能让代码可运行,而模型喜欢可运行的代码。请把密钥完全排除在源码之外。

环境变量的写法模式​

# .env.example -- commit this, it documents the shape without real values
DATABASE_URL=postgres://user:password@localhost:5432/app
SESSION_SECRET=replace-me
STRIPE_SECRET_KEY=replace-me
# Load locally, never commit the real .env
cp .env.example .env
# .env -- gitignored, contains the real values and stays on your machine
DATABASE_URL=postgres://realuser:realpass@prod-host:5432/app
SESSION_SECRET=a-long-random-value-from-a-secret-manager
STRIPE_SECRET_KEY=sk_live_the_actual_key

务必在第一次提交之前把 .env 及相关文件加入 .gitignore,而不是在之后。

# .gitignore
.env
.env.local
.env.*.local
*.pem
*.key

使用 gitleaks 进行提交前密钥扫描​

提交前钩子能在密钥进入仓库历史之前拦住它,这比轮换一个已泄露的密钥便宜得多。

# Install gitleaks (Homebrew example)
brew install gitleaks

# Scan the whole repo once
gitleaks detect --source . --verbose

# Or install it as a pre-commit hook
# .pre-commit-config.yaml
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.0
hooks:
- id: gitleaks
pre-commit install
pre-commit run --all-files
先轮换,后清理

如果某个真实密钥真的进入了提交,请立即轮换该凭据。删除那一行或改写历史,都无法让一个已经推送到远端的密钥重新变成秘密。

手动验证认证与授权​

自动化扫描会漏掉逻辑缺陷,因此你仍然必须手动验证流程。请针对一个正在运行的实例执行以下检查。

会话与认证检查​

  • 退出登录,然后用旧 token 请求一个受保护路由。它必须以 401 失败,而不是返回数据。
  • 在 URL 或请求体里把你自己的用户 ID 改成另一个用户的 ID。服务端必须拒绝它,而不是信任该参数。
  • 以非管理员身份调用一个管理员路由。无论客户端提供什么标志,它都必须返回 403。
  • 确认 Cookie 设置了 httpOnly、secure 以及过期时间。打开浏览器控制台,尝试用 document.cookie 读取该 Cookie;它不应该出现。
  • 重置一个密码,然后验证旧会话已被失效。

Token 与请求头检查​

# A protected route without credentials must not return data
curl -i https://api.example.com/me

# A protected route with a forged or expired token must fail
curl -i https://api.example.com/me -H "Authorization: Bearer forged.token.value"

# An admin route as a normal user must return 403
curl -i https://api.example.com/admin/users -H "Authorization: Bearer $USER_TOKEN"

请针对一个与生产环境一致的预发布环境运行这些检查,而不只是针对数据可随意丢弃的 localhost。

依赖与供应链扫描​

你自己的代码只是攻击面的一部分。生成的代码会引入依赖,而一个有漏洞的包就是你已经发布出去的漏洞。每次改动时都要扫描依赖树。

# Node / npm -- built in, no extra install
npm audit
npm audit --production

# Python
pip-audit
pip-audit -r requirements.txt
# Snyk for a cross-language view
npm install -g snyk
snyk test
snyk monitor

# Or run the Snyk CLI against a Python project
pip install snyk
snyk test --file=requirements.txt

把扫描器接入 CI,让它在每个拉取请求上运行,而不是依赖记忆。

# .github/workflows/security.yml (excerpt)
- name: Scan dependencies
run: npm audit --production --audit-level=high
固定并审查新的依赖

在接受模型建议的某个包之前,请检查它的下载量、最近发布日期和维护者。仿冒名称的包(typosquatting)是真实的攻击途径,而模型可能会建议一个与真实库相近但并非真实库的名字。

部署前安全检查清单​

每次部署前都执行这份清单。它刻意保持简短,以便你真的会去用它。

  • 任何已提交的文件中都没有硬编码的密钥、API 密钥或 token
  • .env 和密钥文件都在 .gitignore 中,且从未被提交过
  • 所有 SQL 都使用参数化查询或 ORM,没有任何字符串拼接
  • 所有用户输入在使用前都经过 schema 校验
  • 会话 token 存储在 httpOnly Cookie 中,而不是 localStorage
  • 每个受保护路由在未认证或未授权时都返回 401/403
  • 管理员路由在服务端校验角色,绝不依赖客户端提供的值
  • CORS 只允许你已知的来源,而不是带凭据的 *
  • npm audit / pip-audit 没有报告高危或严重级别的问题
  • 错误响应不泄露堆栈跟踪、SQL 或内部路径
  • 日志不记录 token、密码或完整的请求体
  • 本冲刺中智能体触及的每个文件都跑过一遍安全审查

上报与修复发现的问题​

只有被修复和跟踪,发现才有价值。给每一项发现足够用来行动的结构。

  1. 记录该发现。 一行写明文件、行号、漏洞类型和严重程度。
  2. 把修复写成 diff。 让 Cursor 产出从脆弱行到修复行的改动,然后审查它。
  3. 添加一个能证明修复有效的测试。 针对注入或认证绕过写一个回归测试,防止它卷土重来。
  4. 更新规则。 如果这个缺陷是智能体产生的,就往 .cursor/rules/security.mdc 里加一行,让它不会再犯。
  5. 重新测试并关闭。 针对修复后的构建重新运行手动检查和扫描器。
Finding: CWE-89 SQL injection, src/api/users.ts:42, severity high
Vulnerable: db.query("SELECT * FROM users WHERE id = " + id)
Fixed: db.query("SELECT * FROM users WHERE id = $1", [id])
Regression test: request /user?id=1' OR '1'='1 returns no rows
Rule update: added "never concatenate user input into SQL" to security.mdc
Status: fixed, verified in staging

把规则文件当作智能体犯过的每一个错误的持续记录。随着时间推移,它会成为仓库中最有价值的安全资产,因为它精确编码了你的代码库过去曾经出错的每一种方式。