본문으로 건너뛰기

Cursor로 바이브 코딩을 할 때의 보안 사고방식

Vibe coding security with Cursor

Cursor로 바이브 코딩하는 것은 빠르고 재미있습니다: 원하는 것을 설명하면 에이전트가 코드를 작성하고, 모든 줄을 읽기 위해 멈추지 않고 흐름을 이어갑니다. 문제는 프롬프트를 만족하도록 학습된 모델이 작동하고 깔끔해 보이지만 여전히 SQL 인젝션, 하드코딩된 API 키 또는 클라이언트를 신뢰하는 인가 검사를 지닌 코드를 기꺼이 만들어낸다는 것입니다. 이 가이드는 Cursor의 바이브 코딩 워크플로를 위한 반복 가능한 보안 사고방식을 제공하며, AI 생성 코드가 가장 자주 만들어내는 실패를 발견하는 것부터 출력 감사, 규칙 강제, 시크릿 처리, 그리고 어떤 것이든 배포되기 전 배포 전 체크리스트 실행까지 다룹니다.

바이브 코딩이란 무엇이며 보안이 중요한 이유​

바이브 코딩은 의도를 가지고 조종하고 구현 세부 사항은 모델이 채우게 하는 것입니다. "세션 쿠키가 있는 로그인 엔드포인트"를 요청하면 Cursor가 라우트, 데이터베이스 호출 및 미들웨어를 작성합니다. 출력은 보통 기능하지만, 기능하는 것이 안전한 것과 같지는 않습니다. 모델은 코드를 실행되게 만드는 데 최적화되어 있지, 같은 코드를 읽는 공격자에 대비하는 데 최적화되어 있지 않습니다.

이 워크플로에서는 보안이 덜 중요한 것이 아니라 더 중요합니다. 세 가지 이유가 있습니다:

  • 검토를 덜 합니다. 모델이 프롬프트당 수십 줄을 작성하면 인간 검토 단계가 병목이 되어 건너뛰게 됩니다.
  • 모델은 안전하지 않은 코드에서 학습했습니다. 공개 저장소에는 문자열 연결 SQL과 클라이언트 측 토큰 저장이 넘쳐서 그런 패턴이 완성에 나타납니다.
  • 영향 범위가 큽니다. Agent 모드는 한 번에 많은 파일을 건드릴 수 있으므로 안전하지 않은 패턴 하나가 코드베이스 전체에 복제됩니다.

보안은 마지막에 덧붙이는 별개의 단계가 아닙니다. Cursor로 프롬프트하고, 검토하고, 배포하는 방식에 내장하는 상시 습관입니다.

검토자는 여전히 당신입니다

Cursor의 에이전트는 컴파일되고 테스트를 통과하면서도 취약점을 배포하는 코드를 작성할 수 있습니다. 모델은 당신의 리스크에 서명할 수 없습니다. 생성된 모든 블록을 빠르지만 부주의한 주니어 개발자의 초안으로 취급하세요.

AI 생성 코드에서 가장 흔한 취약점​

다음은 AI 생성 코드에서 거듭 나타나는 실패 유형입니다. 모든 줄을 읽는 대신 훑어보면서 발견할 수 있도록 각각의 형태를 인식하는 법을 배우세요.

취약점생성 코드에서의 모습모델이 만들어내는 이유
SQL 인젝션매개변수 대신 문자열 연결로 만든 쿼리연결이 작성하기 더 간단하고 "자연스럽게" 읽힘
XSS삭제되지 않은 사용자 입력을 innerHTML 또는 dangerouslySetInnerHTML로 렌더링HTML을 직접 렌더링하는 것이 시각적 결과로 가는 가장 짧은 경로
하드코딩된 시크릿 / API 키소스 파일에 남겨진 const API_KEY = "sk-live-..."리터럴 값이 스니펫을 자체 포함적이고 실행 가능하게 만듦
취약한 인증 / 세션 처리localStorage에 저장된 토큰, 만료 없음, 서버 측 검증 없음클라이언트 저장이 브라우저에서 시연하기 가장 쉬움
허용적 CORS자격 증명과 함께 Access-Control-Allow-Origin: *와일드카드가 즉각적인 오류를 멈추는 한 줄 수정
안전하지 않은 역직렬화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에 저장하면 페이지의 모든 스크립트가 읽을 수 있게 되어, 단일 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
먼저 교체하고, 그다음 정리하세요

실제 시크릿이 커밋에 들어가면 즉시 자격 증명을 교체하세요. 줄을 삭제하거나 기록을 다시 쓰는 것은 이미 원격에 푸시된 키의 유출을 되돌리지 못합니다.

인증 및 인가를 수동으로 검증하기​

자동 스캔은 로직 버그를 놓치므로 여전히 흐름을 손으로 검증해야 합니다. 실행 중인 인스턴스에 대해 다음 검사를 진행하세요.

세션 및 인증 검사​

  • 로그아웃한 다음 이전 토큰으로 보호된 라우트를 요청하세요. 데이터를 반환하지 않고 401로 실패해야 합니다.
  • URL이나 본문에서 자신의 사용자 ID를 다른 사용자의 ID로 바꾸세요. 서버는 매개변수를 신뢰하지 않고 이를 거부해야 합니다.
  • 비관리자로 관리자 라우트를 호출하세요. 클라이언트가 제공한 플래그와 무관하게 403을 반환해야 합니다.
  • 쿠키가 httpOnly, secure 및 만료와 함께 설정되는지 확인하세요. 브라우저 콘솔을 열고 document.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에 대해서만이 아닙니다.

의존성 및 공급망 스캔​

자체 코드는 표면의 일부일 뿐입니다. 생성된 코드는 의존성을 끌어들이고, 취약한 패키지는 배포한 취약점입니다. 모든 변경에서 트리를 스캔하세요.

# 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 쿠키에 저장됨
  • 모든 보호된 라우트가 미인증 또는 미인가 시 401/403을 반환
  • 관리자 라우트가 클라이언트 제공 값이 아니라 서버 측에서 역할을 검증
  • CORS가 자격 증명과 함께 *가 아닌 알려진 오리진만 허용
  • npm audit / pip-audit가 높음 또는 심각한 발견 사항을 보고하지 않음
  • 오류 응답이 스택 트레이스, SQL 또는 내부 경로를 유출하지 않음
  • 로깅이 토큰, 비밀번호 또는 전체 요청 본문을 기록하지 않음
  • 이번 스프린트에 에이전트가 건드린 모든 파일에 보안 검토 패스가 실행됨

발견 사항 보고 및 수정​

발견 사항은 수정되고 추적될 때만 유용합니다. 각각에 행동할 수 있을 만큼만 구조를 부여하세요.

  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

규칙 파일을 에이전트가 저지른 모든 실수의 성장하는 기록으로 취급하세요. 시간이 지나면 코드베이스가 이전에 잘못되었던 방식을 정확히 인코딩하므로 저장소에서 가장 가치 있는 보안 자산이 됩니다.