Stripe 웹훅 보안
이 스킬은 vibe-preflight 스캐너(scripts/preflight.mjs, 이 파일과 같은 디렉토리, Node 20+, 의존성 없음, 네트워크 불필요)의 Stripe 부분을 실행합니다. Next.js 라우트 핸들러, pages/api, Express 및 Supabase Edge Functions에서 웹훅 핸들러를 찾아 코드의 두 가지 항목을 검사합니다. 이 페이지의 나머지 부분은 핸들러를 읽어 수행하는 검토 항목입니다. 패턴 스캔만으로는 완벽히 증명할 수 없기 때문입니다.
1. 스캔 실행
프로젝트 루트에서 다음을 실행합니다.
node ${CLAUDE_SKILL_DIR}/scripts/preflight.mjs . --only stripe --fail-on warning --format json
Claude Code는 ${CLAUDE_SKILL_DIR}을 자동으로 채웁니다. 다른 에이전트의 경우 이 스킬의 폴더(예: .agents/skills/stripe-webhook-security 또는 .claude/skills/stripe-webhook-security)를 사용하세요.
| 규칙 | 수준 | 설명 |
|---|---|---|
stripe-webhook-unverified | error | 핸들러가 Stripe 이벤트를 처리하면서 constructEvent / constructEventAsync를 호출하지 않습니다. URL을 찾은 누구나 가짜 checkout.session.completed를 POST할 수 있습니다. |
stripe-webhook-parsed-body | warning | 서명은 확인했지만, 이미 파싱된 본문에 대해 확인했습니다. Stripe는 전송한 정확한 바이트에 서명하므로 실제 이벤트는 400 오류와 함께 실패하고 누군가 결국 검사를 삭제하게 됩니다. |
--only stripe는 Stripe 이벤트 이름, stripe-signature 헤더 읽기, 또는 Stripe를 언급하는 파일의 웹훅 경로를 통해 핸들러를 찾습니다. 0 errors를 보고하지만 사용자가 웹훅을 알고 있다면 핸들러를 직접 찾아 아래 검토를 수행하세요.
2. 검증 수정
라우트 핸들러 (Next.js App Router, Remix, Hono 및 기타 fetch 스타일 핸들러):
export async function POST(request: Request) {
const body = await request.text(); // 원시 본문, 절대 request.json() 사용 금지
const signature = request.headers.get("stripe-signature");
if (!signature) return new Response("Missing signature", { status: 400 });
let event: Stripe.Event;
try {
event = stripe.webhooks.constructEvent(body, signature, process.env.STRIPE_WEBHOOK_SECRET!);
} catch {
return new Response("Invalid signature", { status: 400 });
}
// 이벤트 처리
return Response.json({ received: true });
}
Express: app.use(express.json()) 이전에 express.raw({ type: "application/json" })로 웹훅 라우트를 등록하고, req.body (이때는 Buffer)를 constructEvent에 전달하세요.
Next.js pages/api: export const config = { api: { bodyParser: false } };로 설정하고 검증 전에 원시 요청을 Buffer로 읽으세요.
Supabase Edge Functions 및 기타 Deno / 엣지 런타임:
await stripe.webhooks.constructEventAsync(body, signature, secret, undefined, Stripe.createSubtleCryptoProvider())를 사용하세요.
3. 핸들러를 읽어 나머지 검토
스캐너는 다음 항목을 확인하지 않습니다. 코드를 살펴보고 각 항목에 대해 발견한 내용을 사용자에게 알려주세요.
- 비밀 키.
STRIPE_WEBHOOK_SECRET(whsec_...)은 서버 환경 변수에서 가져와야 하며, 절대NEXT_PUBLIC_/VITE_변수나 소스 코드에 포함시키지 마세요. 테스트 모드와 라이브 모드는 서로 다른 비밀 키를 가지며, 각 엔드포인트와stripe listenCLI도 각각 다른 비밀 키를 사용합니다. - 재생 공격 방지.
constructEvent는 허용 오차(기본값 300초)보다 오래된 서명을 거부합니다. tolerance 인자는 생략하거나 작게 유지하세요. 오류를 잡고 계속 진행하지 마십시오. - 멱등성. Stripe는 동일한 이벤트를 두 번 이상 전달할 수 있습니다. 쓰기 작업을 자연스럽게 반복 가능하게 만들거나(Stripe ID를 키로 하는 upsert, 크레딧을 추가하는 대신 상태 설정),
event.id를 고유 제약 조건이 있는 테이블에 저장하고 이미 본 ID는 건너뛰세요. 크레딧, 잔액, 재고를 추가하는 부분을 특히 주의 깊게 살펴보세요. - 순서. 이벤트가 순서대로 도착하지 않을 수 있습니다. 구독의 경우 이벤트 순서를 신뢰하는 대신 API(
stripe.subscriptions.retrieve)에서 현재 객체를 가져오거나 타임스탬프를 비교하세요. - 신뢰. 사용자와 계획은 검증된 이벤트(
client_reference_id,metadata, 체크아웃 시 저장한 고객 ID)에서 가져오고, 절대 쿼리 문자열 또는 두 번째 서명되지 않은 요청에서 가져오지 마세요. 성공 페이지 리디렉션은 아무것도 증명하지 않습니다. 웹훅에서 액세스 권한을 부여하세요. - 응답. 이벤트가 저장되거나 처리되면 즉시 2xx를 반환하세요. 느린 작업은 큐로 보내세요. 잘못된 서명에 대해서만 400을 반환하여 Stripe의 재시도가 의미를 가지도록 하세요.
- 이벤트 목록. 엔드포인트를 코드가 처리하는 이벤트에만 구독하고, 다른 유형은 오류 없이 무시하세요.
4. 보고
스캐너 결과를 나열하고, 각 검토 항목에 대해 한 줄씩 작성하세요: 양호( fine ), 수정됨( fixed ), 또는 사용자에 대한 질문(예: "크레딧이 모든 전달에 추가됩니다: 반복된 이벤트가 다시 추가되어야 합니까?"). 수정 후 스캔을 다시 실행하고 마지막 줄을 보여주세요. 실제 테스트를 위해 stripe listen --forward-to localhost:3000/api/webhooks와 stripe trigger checkout.session.completed(Stripe CLI 사용)를 제안하세요.
확인된 오탐은 해당 줄 또는 위 줄에 // vibe-preflight-ignore <rule-id>를 추가하고 주석에 이유를 기재하여 무음 처리할 수 있습니다.