メインコンテンツまでスキップ

Cursor でのバイブコーディングのためのセキュリティマインドセット

Vibe coding security with Cursor

Cursor でのバイブコーディングは速くて楽しいものです。欲しいものを説明するとエージェントがコードを書き、あなたはすべての行を読むために止まることなく流れを続けます。問題は、プロンプトを満たすようにトレーニングされたモデルが、動作し見た目もきれいなコードを喜んで生成する一方で、SQL インジェクション、ハードコードされた API キー、クライアントを信頼する認可チェックをまだ抱えていることです。本ガイドでは、Cursor でのバイブコーディングワークフローのための再現可能なセキュリティマインドセットを提供します。AI 生成コードが最もよく生み出す失敗の見つけ方から、出力の監査、ルールの適用、シークレットの扱い、そして何かを出荷する前のデプロイ前チェックリストの実行までを扱います。

バイブコーディングとは何か、なぜセキュリティが重要か​

バイブコーディングとは、意図をもって舵を取り、実装の詳細はモデルに埋めてもらうことを意味します。「セッションクッキー付きのログインエンドポイント」を求めると、Cursor がルート、データベース呼び出し、ミドルウェアを書きます。出力は通常機能的ですが、機能的であることは安全であることと同じではありません。モデルはコードを動かすことに最適化しており、同じコードを読む攻撃者に防御することには最適化していません。

このワークフローではセキュリティはより重要であり、決して軽くなりません。理由は 3 つあります:

  • レビューが少なくなる。 モデルがプロンプトごとに数十行を書くと、人間のレビューステップがボトルネックになり省略されます。
  • モデルは安全でないコードから学習した。 公開リポジトリには文字列連結された SQL やクライアント側のトークン保存が大量にあり、それらのパターンが補完に現れます。
  • 影響範囲が大きい。 Agent モードは一度に多くのファイルに触れるため、1 つの安全でないパターンがコードベース全体に複製されます。

セキュリティは最後に付け足す別個のフェーズではありません。Cursor でプロンプトし、レビューし、デプロイする方法に組み込む常時の習慣です。

レビューするのは依然としてあなたです

Cursor のエージェントは、コンパイルが通り、テストも通るが、脆弱性を出荷するコードを書くことができます。モデルはあなたのリスクに署名できません。生成されたすべてのブロックを、高速だが不注意なジュニア開発者からの下書きとして扱ってください。

AI 生成コードに最もよくある脆弱性​

これらは AI 生成コードで繰り返し現れる失敗パターンです。すべての行を読むのではなく、ざっと目を通すときに気づけるよう、それぞれの形を認識できるようにしてください。

脆弱性生成コードでの見え方モデルが生成する理由
SQL インジェクションパラメータではなく文字列連結で組み立てられたクエリ連結の方が書くのが簡単で「自然」に読める
XSSinnerHTML や dangerouslySetInnerHTML でレンダリングされた未サニタイズのユーザー入力HTML を直接レンダリングするのが視覚的結果への最短経路
ハードコードされたシークレット / API キーソースファイルに残された const API_KEY = "sk-live-..."リテラル値があるとスニペットが自己完結して実行可能になる
壊れた認証 / セッション処理localStorage に保存されたトークン、有効期限なし、サーバー側検証なしクライアント保存はブラウザでデモするのが最も簡単
緩い CORS認証情報付きの Access-Control-Allow-Origin: *ワイルドカードは目先のエラーを止める 1 行の修正
安全でないデシリアライゼーションpickle.loads、eval、または JSON を直接オブジェクトとして信頼することすべてをデシリアライズするのは便利で、入力が信頼できると仮定する
露出した管理エンドポイントロールチェックのない /admin/users のようなルートモデルは機能を実装するがガードを忘れる
入力検証の欠如スキーマチェックなしでロジックに直接使われるボディハッピーパスがすでに動くとき検証は冗長に感じられる

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()

セッショントークンを localStorage に保存すると、ページ上のあらゆるスクリプトから読めるようになり、1 つの 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 生成コードを監査する​

監査とはすべての行を読むことではありません。最初のパスが書いたものに攻撃させるよう、モデルの 2 回目のパスを求めることです。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/ のルールはコンテキストに読み込まれるため、エージェントが作業するときセキュリティチェックリストが常に存在します。

すべてのエージェントセッション後に監査する

認証、入力処理、シークレットに触れるエージェントセッションの後は、「セキュリティレビューを実行する」を反射的に行ってください。プロンプト 1 つ分のコストで、上の表の失敗パターンがコミットに到達する前に捕まえられます。

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
先にローテーション、次にクリーンアップ

実際のシークレットがコミットに入ってしまった場合、認証情報をすぐにローテーションしてください。行を削除したり履歴を書き換えても、すでにリモートにプッシュされたキーの漏洩は取り消せません。

認証と認可を手動で検証する​

自動スキャンはロジックのバグを見逃すため、フローを手作業で検証する必要があります。実行中のインスタンスに対してこれらのチェックを行ってください。

セッションと認証のチェック​

  • ログアウトしてから、古いトークンで保護されたルートをリクエストします。データを返すのではなく、401 で失敗しなければなりません。
  • URL やボディ内の自分のユーザー ID を別のユーザーの ID に変更します。サーバーはパラメータを信頼せず、それを拒否しなければなりません。
  • 非管理者として管理ルートを呼び出します。クライアントから供給されたフラグに関係なく 403 を返さなければなりません。
  • Cookie が httpOnly、secure、有効期限付きで設定されていることを確認します。ブラウザコンソールを開き document.cookie で Cookie を読もうとしてみてください。表示されないはずです。
  • パスワードをリセットし、古いセッションが無効化されることを検証します。

トークンとヘッダーのチェック​

# 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 だけでなく、本番を模したステージング環境に対して実行してください。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
新しい依存関係を固定してレビューする

モデルが提案したパッケージを受け入れる前に、ダウンロード数、最終公開日、メンテナーを確認してください。タイポスクワッティングされたパッケージは現実の攻撃ベクトルであり、モデルは本物のライブラリに近いが実在しない名前を提案することがあります。

デプロイ前のセキュリティチェックリスト​

毎回のデプロイ前にこのリストを実行してください。実際に使えるよう意図的に短くしています。

  • コミットされたどのファイルにもハードコードされたシークレット、API キー、トークンがない
  • .env とキーファイルが .gitignore にあり、一度もコミットされていない
  • すべての SQL がパラメータ化クエリまたは ORM を使用し、文字列連結がない
  • すべてのユーザー入力が使用前にスキーマに対して検証される
  • セッショントークンが localStorage ではなく httpOnly Cookie に保存される
  • すべての保護されたルートが未認証または未認可時に 401/403 を返す
  • 管理ルートがクライアント供給の値ではなくサーバー側でロールを検証する
  • CORS が既知のオリジンのみを許可し、認証情報付きの * ではない
  • npm audit / pip-audit が高または重大な所見を報告しない
  • エラー応答がスタックトレース、SQL、内部パスを漏らさない
  • ログがトークン、パスワード、完全なリクエストボディを記録しない
  • このスプリントでエージェントが触れたすべてのファイルに対してセキュリティレビューパスが実行された

所見の報告と修正​

所見は修正され追跡されて初めて役に立ちます。それぞれに、行動に移せるだけのちょうどの構造を与えてください。

  1. 所見を記録する。 ファイル、行、脆弱性の種類、重大度を含む 1 行。
  2. 修正を差分として書く。 Cursor に脆弱な行から修正行への変更を生成させ、レビューします。
  3. 修正を証明するテストを追加する。 インジェクションまたは認可バイパスの回帰テストが、それが再発するのを防ぎます。
  4. ルールを更新する。 エージェントがバグを生み出した場合、.cursor/rules/security.mdc に 1 行追加して二度と生み出さないようにします。
  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

ルールファイルを、エージェントがこれまでに犯したすべての誤りの成長する記録として扱ってください。時間が経つにつれて、あなたのコードベースが以前どのように間違っていたかを正確にエンコードしているため、リポジトリで最も価値のあるセキュリティ資産になります。