[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7BF2AVFT6E2S7RF8P54CD9Y":3,"skill-same:01M7BF2AVFT6E2S7RF8P54CD9Y":54},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":19,"linkOnly":20,"markdownContent":21,"markdownBody":22,"frontMatter":23,"refName":25,"commitSha":26,"skillDirectory":27,"forksCount":28,"repositoryHtmlUrl":29,"repositoryHomepageUrl":30,"ownerHtmlUrl":31,"ownerBlogUrl":6,"ownerDescription":32,"updatedAt":33,"install":34,"resources":47},"01M7BF2AVFT6E2S7RF8P54CD9Y","security-review",null,"인증 추가, 사용자 입력 처리, 시크릿 관리, API 엔드포인트 생성, 결제\u002F민감한 기능 구현 시 이 스킬을 사용하세요. 포괄적인 보안 체크리스트와 패턴을 제공합니다.","YYC-Cube\u002FYYC3-Knowledge-Base","YYC-Cube","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F235006543?v=4","01M79J2PH4SWD2X0825X6PN748","yyc3","01M79J2PHVK3F6ZBYT8K2SCZCW",1,"MIT","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fko-KR\u002Fskills\u002Fsecurity-review\u002FSKILL.md",[],[],"2026-11-15 14:19:17",false,"---\nname: security-review\ndescription: 인증 추가, 사용자 입력 처리, 시크릿 관리, API 엔드포인트 생성, 결제\u002F민감한 기능 구현 시 이 스킬을 사용하세요. 포괄적인 보안 체크리스트와 패턴을 제공합니다.\norigin: ECC\n---\n\n# 보안 리뷰 스킬\n\n이 스킬은 모든 코드가 보안 모범 사례를 따르고 잠재적 취약점을 식별하도록 보장합니다.\n\n## 활성화 시점\n\n- 인증 또는 권한 부여 구현 시\n- 사용자 입력 또는 파일 업로드 처리 시\n- 새로운 API 엔드포인트 생성 시\n- 시크릿 또는 자격 증명 작업 시\n- 결제 기능 구현 시\n- 민감한 데이터 저장 또는 전송 시\n- 서드파티 API 통합 시\n\n## 보안 체크리스트\n\n### 1. 시크릿 관리\n\n#### 절대 하지 말아야 할 것\n```typescript\nconst apiKey = \"sk-proj-xxxxx\"  \u002F\u002F Hardcoded secret\nconst dbPassword = \"password123\" \u002F\u002F In source code\n```\n\n#### 반드시 해야 할 것\n```typescript\nconst apiKey = process.env.OPENAI_API_KEY\nconst dbUrl = process.env.DATABASE_URL\n\n\u002F\u002F Verify secrets exist\nif (!apiKey) {\n  throw new Error('OPENAI_API_KEY not configured')\n}\n```\n\n#### 확인 단계\n- [ ] 하드코딩된 API 키, 토큰, 비밀번호 없음\n- [ ] 모든 시크릿이 환경 변수에 저장됨\n- [ ] `.env.local`이 .gitignore에 포함됨\n- [ ] git 히스토리에 시크릿 없음\n- [ ] 프로덕션 시크릿이 호스팅 플랫폼(Vercel, Railway)에 저장됨\n\n### 2. 입력 유효성 검사\n\n#### 항상 사용자 입력을 검증할 것\n```typescript\nimport { z } from 'zod'\n\n\u002F\u002F Define validation schema\nconst CreateUserSchema = z.object({\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n  age: z.number().int().min(0).max(150)\n})\n\n\u002F\u002F Validate before processing\nexport async function createUser(input: unknown) {\n  try {\n    const validated = CreateUserSchema.parse(input)\n    return await db.users.create(validated)\n  } catch (error) {\n    if (error instanceof z.ZodError) {\n      return { success: false, errors: error.errors }\n    }\n    throw error\n  }\n}\n```\n\n#### 파일 업로드 유효성 검사\n```typescript\nfunction validateFileUpload(file: File) {\n  \u002F\u002F Size check (5MB max)\n  const maxSize = 5 * 1024 * 1024\n  if (file.size > maxSize) {\n    throw new Error('File too large (max 5MB)')\n  }\n\n  \u002F\u002F Type check\n  const allowedTypes = ['image\u002Fjpeg', 'image\u002Fpng', 'image\u002Fgif']\n  if (!allowedTypes.includes(file.type)) {\n    throw new Error('Invalid file type')\n  }\n\n  \u002F\u002F Extension check\n  const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']\n  const extension = file.name.toLowerCase().match(\u002F\\.[^.]+$\u002F)?.[0]\n  if (!extension || !allowedExtensions.includes(extension)) {\n    throw new Error('Invalid file extension')\n  }\n\n  return true\n}\n```\n\n#### 확인 단계\n- [ ] 모든 사용자 입력이 스키마로 검증됨\n- [ ] 파일 업로드가 제한됨 (크기, 타입, 확장자)\n- [ ] 사용자 입력이 쿼리에 직접 사용되지 않음\n- [ ] 화이트리스트 검증 사용 (블랙리스트가 아닌)\n- [ ] 에러 메시지가 민감한 정보를 노출하지 않음\n\n### 3. SQL Injection 방지\n\n#### 절대 SQL을 연결하지 말 것\n```typescript\n\u002F\u002F DANGEROUS - SQL Injection vulnerability\nconst query = `SELECT * FROM users WHERE email = '${userEmail}'`\nawait db.query(query)\n```\n\n#### 반드시 파라미터화된 쿼리를 사용할 것\n```typescript\n\u002F\u002F Safe - parameterized query\nconst { data } = await supabase\n  .from('users')\n  .select('*')\n  .eq('email', userEmail)\n\n\u002F\u002F Or with raw SQL\nawait db.query(\n  'SELECT * FROM users WHERE email = $1',\n  [userEmail]\n)\n```\n\n#### 확인 단계\n- [ ] 모든 데이터베이스 쿼리가 파라미터화된 쿼리 사용\n- [ ] SQL에서 문자열 연결 없음\n- [ ] ORM\u002F쿼리 빌더가 올바르게 사용됨\n- [ ] Supabase 쿼리가 적절히 새니타이징됨\n\n### 4. 인증 및 권한 부여\n\n#### JWT 토큰 처리\n```typescript\n\u002F\u002F FAIL: WRONG: localStorage (vulnerable to XSS)\nlocalStorage.setItem('token', token)\n\n\u002F\u002F PASS: CORRECT: httpOnly cookies\nres.setHeader('Set-Cookie',\n  `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)\n```\n\n#### 권한 부여 확인\n```typescript\nexport async function deleteUser(userId: string, requesterId: string) {\n  \u002F\u002F ALWAYS verify authorization first\n  const requester = await db.users.findUnique({\n    where: { id: requesterId }\n  })\n\n  if (requester.role !== 'admin') {\n    return NextResponse.json(\n      { error: 'Unauthorized' },\n      { status: 403 }\n    )\n  }\n\n  \u002F\u002F Proceed with deletion\n  await db.users.delete({ where: { id: userId } })\n}\n```\n\n#### Row Level Security (Supabase)\n```sql\n-- Enable RLS on all tables\nALTER TABLE users ENABLE ROW LEVEL SECURITY;\n\n-- Users can only view their own data\nCREATE POLICY \"Users view own data\"\n  ON users FOR SELECT\n  USING (auth.uid() = id);\n\n-- Users can only update their own data\nCREATE POLICY \"Users update own data\"\n  ON users FOR UPDATE\n  USING (auth.uid() = id);\n```\n\n#### 확인 단계\n- [ ] 토큰이 httpOnly 쿠키에 저장됨 (localStorage가 아닌)\n- [ ] 민감한 작업 전에 권한 부여 확인\n- [ ] Supabase에서 Row Level Security 활성화됨\n- [ ] 역할 기반 접근 제어 구현됨\n- [ ] 세션 관리가 안전함\n\n### 5. XSS 방지\n\n#### HTML 새니타이징\n```typescript\nimport DOMPurify from 'isomorphic-dompurify'\n\n\u002F\u002F ALWAYS sanitize user-provided HTML\nfunction renderUserContent(html: string) {\n  const clean = DOMPurify.sanitize(html, {\n    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],\n    ALLOWED_ATTR: []\n  })\n  return \u003Cdiv dangerouslySetInnerHTML={{ __html: clean }} \u002F>\n}\n```\n\n#### Content Security Policy\n```typescript\n\u002F\u002F next.config.js\nconst securityHeaders = [\n  {\n    key: 'Content-Security-Policy',\n    value: `\n      default-src 'self';\n      script-src 'self' 'nonce-{nonce}';\n      style-src 'self' 'nonce-{nonce}';\n      img-src 'self' data: https:;\n      font-src 'self';\n      connect-src 'self' https:\u002F\u002Fapi.example.com;\n    `.replace(\u002F\\s{2,}\u002Fg, ' ').trim()\n  }\n]\n```\n\n`{nonce}`는 요청마다 새로 생성하고, 헤더와 인라인 `\u003Cscript>`\u002F`\u003Cstyle>` 태그에 동일하게 주입해야 합니다.\n\n#### 확인 단계\n- [ ] 사용자 제공 HTML이 새니타이징됨\n- [ ] CSP 헤더가 구성됨\n- [ ] 검증되지 않은 동적 콘텐츠 렌더링 없음\n- [ ] React의 내장 XSS 보호가 사용됨\n\n### 6. CSRF 보호\n\n#### CSRF 토큰\n```typescript\nimport { csrf } from '@\u002Flib\u002Fcsrf'\n\nexport async function POST(request: Request) {\n  const token = request.headers.get('X-CSRF-Token')\n\n  if (!csrf.verify(token)) {\n    return NextResponse.json(\n      { error: 'Invalid CSRF token' },\n      { status: 403 }\n    )\n  }\n\n  \u002F\u002F Process request\n}\n```\n\n#### SameSite 쿠키\n```typescript\nres.setHeader('Set-Cookie',\n  `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)\n```\n\n#### 확인 단계\n- [ ] 상태 변경 작업에 CSRF 토큰 적용\n- [ ] 모든 쿠키에 SameSite=Strict 설정\n- [ ] Double-submit 쿠키 패턴 구현\n\n### 7. 속도 제한\n\n#### API 속도 제한\n```typescript\nimport rateLimit from 'express-rate-limit'\n\nconst limiter = rateLimit({\n  windowMs: 15 * 60 * 1000, \u002F\u002F 15 minutes\n  max: 100, \u002F\u002F 100 requests per window\n  message: 'Too many requests'\n})\n\n\u002F\u002F Apply to routes\napp.use('\u002Fapi\u002F', limiter)\n```\n\n#### 비용이 높은 작업\n```typescript\n\u002F\u002F Aggressive rate limiting for searches\nconst searchLimiter = rateLimit({\n  windowMs: 60 * 1000, \u002F\u002F 1 minute\n  max: 10, \u002F\u002F 10 requests per minute\n  message: 'Too many search requests'\n})\n\napp.use('\u002Fapi\u002Fsearch', searchLimiter)\n```\n\n#### 확인 단계\n- [ ] 모든 API 엔드포인트에 속도 제한 적용\n- [ ] 비용이 높은 작업에 더 엄격한 제한\n- [ ] IP 기반 속도 제한\n- [ ] 사용자 기반 속도 제한 (인증된 사용자)\n\n### 8. 민감한 데이터 노출\n\n#### 로깅\n```typescript\n\u002F\u002F FAIL: WRONG: Logging sensitive data\nconsole.log('User login:', { email, password })\nconsole.log('Payment:', { cardNumber, cvv })\n\n\u002F\u002F PASS: CORRECT: Redact sensitive data\nconsole.log('User login:', { email, userId })\nconsole.log('Payment:', { last4: card.last4, userId })\n```\n\n#### 에러 메시지\n```typescript\n\u002F\u002F FAIL: WRONG: Exposing internal details\ncatch (error) {\n  return NextResponse.json(\n    { error: error.message, stack: error.stack },\n    { status: 500 }\n  )\n}\n\n\u002F\u002F PASS: CORRECT: Generic error messages\ncatch (error) {\n  console.error('Internal error:', error)\n  return NextResponse.json(\n    { error: 'An error occurred. Please try again.' },\n    { status: 500 }\n  )\n}\n```\n\n#### 확인 단계\n- [ ] 로그에 비밀번호, 토큰, 시크릿 없음\n- [ ] 사용자에게 표시되는 에러 메시지가 일반적임\n- [ ] 상세 에러는 서버 로그에만 기록\n- [ ] 사용자에게 스택 트레이스가 노출되지 않음\n\n### 9. 블록체인 보안 (Solana)\n\n#### 지갑 검증\n```typescript\nimport nacl from 'tweetnacl'\nimport bs58 from 'bs58'\nimport { PublicKey } from '@solana\u002Fweb3.js'\n\nasync function verifyWalletOwnership(\n  publicKey: string,\n  signature: string,\n  message: string\n) {\n  try {\n    const publicKeyBytes = new PublicKey(publicKey).toBytes()\n    const signatureBytes = bs58.decode(signature)\n    const messageBytes = new TextEncoder().encode(message)\n\n    return nacl.sign.detached.verify(\n      messageBytes,\n      signatureBytes,\n      publicKeyBytes\n    )\n  } catch (error) {\n    return false\n  }\n}\n```\n\n참고: Solana 공개 키와 서명은 일반적으로 base64가 아니라 base58로 인코딩됩니다.\n\n#### 트랜잭션 검증\n```typescript\nasync function verifyTransaction(transaction: Transaction) {\n  \u002F\u002F Verify recipient\n  if (transaction.to !== expectedRecipient) {\n    throw new Error('Invalid recipient')\n  }\n\n  \u002F\u002F Verify amount\n  if (transaction.amount > maxAmount) {\n    throw new Error('Amount exceeds limit')\n  }\n\n  \u002F\u002F Verify user has sufficient balance\n  const balance = await getBalance(transaction.from)\n  if (balance \u003C transaction.amount) {\n    throw new Error('Insufficient balance')\n  }\n\n  return true\n}\n```\n\n#### 확인 단계\n- [ ] 지갑 서명 검증됨\n- [ ] 트랜잭션 세부 정보 유효성 검사됨\n- [ ] 트랜잭션 전 잔액 확인\n- [ ] 블라인드 트랜잭션 서명 없음\n\n### 10. 의존성 보안\n\n#### 정기 업데이트\n```bash\n# Check for vulnerabilities\nnpm audit\n\n# Fix automatically fixable issues\nnpm audit fix\n\n# Update dependencies\nnpm update\n\n# Check for outdated packages\nnpm outdated\n```\n\n#### 잠금 파일\n```bash\n# ALWAYS commit lock files\ngit add package-lock.json\n\n# Use in CI\u002FCD for reproducible builds\nnpm ci  # Instead of npm install\n```\n\n#### 확인 단계\n- [ ] 의존성이 최신 상태\n- [ ] 알려진 취약점 없음 (npm audit 클린)\n- [ ] 잠금 파일 커밋됨\n- [ ] GitHub에서 Dependabot 활성화됨\n- [ ] 정기적인 보안 업데이트\n\n## 보안 테스트\n\n### 자동화된 보안 테스트\n```typescript\n\u002F\u002F Test authentication\ntest('requires authentication', async () => {\n  const response = await fetch('\u002Fapi\u002Fprotected')\n  expect(response.status).toBe(401)\n})\n\n\u002F\u002F Test authorization\ntest('requires admin role', async () => {\n  const response = await fetch('\u002Fapi\u002Fadmin', {\n    headers: { Authorization: `Bearer ${userToken}` }\n  })\n  expect(response.status).toBe(403)\n})\n\n\u002F\u002F Test input validation\ntest('rejects invalid input', async () => {\n  const response = await fetch('\u002Fapi\u002Fusers', {\n    method: 'POST',\n    body: JSON.stringify({ email: 'not-an-email' })\n  })\n  expect(response.status).toBe(400)\n})\n\n\u002F\u002F Test rate limiting\ntest('enforces rate limits', async () => {\n  const requests = Array(101).fill(null).map(() =>\n    fetch('\u002Fapi\u002Fendpoint')\n  )\n\n  const responses = await Promise.all(requests)\n  const tooManyRequests = responses.filter(r => r.status === 429)\n\n  expect(tooManyRequests.length).toBeGreaterThan(0)\n})\n```\n\n## 배포 전 보안 체크리스트\n\n모든 프로덕션 배포 전:\n\n- [ ] **시크릿**: 하드코딩된 시크릿 없음, 모두 환경 변수에 저장\n- [ ] **입력 유효성 검사**: 모든 사용자 입력 검증됨\n- [ ] **SQL Injection**: 모든 쿼리 파라미터화됨\n- [ ] **XSS**: 사용자 콘텐츠 새니타이징됨\n- [ ] **CSRF**: 보호 활성화됨\n- [ ] **인증**: 적절한 토큰 처리\n- [ ] **권한 부여**: 역할 확인 적용됨\n- [ ] **속도 제한**: 모든 엔드포인트에서 활성화됨\n- [ ] **HTTPS**: 프로덕션에서 강제 적용\n- [ ] **보안 헤더**: CSP, X-Frame-Options 구성됨\n- [ ] **에러 처리**: 에러에 민감한 데이터 없음\n- [ ] **로깅**: 민감한 데이터가 로그에 없음\n- [ ] **의존성**: 최신 상태, 취약점 없음\n- [ ] **Row Level Security**: Supabase에서 활성화됨\n- [ ] **CORS**: 적절히 구성됨\n- [ ] **파일 업로드**: 유효성 검사됨 (크기, 타입)\n- [ ] **지갑 서명**: 검증됨 (블록체인인 경우)\n\n## 참고 자료\n\n- [OWASP Top 10](https:\u002F\u002Fowasp.org\u002Fwww-project-top-ten\u002F)\n- [Next.js Security](https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fsecurity)\n- [Supabase Security](https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth)\n- [Web Security Academy](https:\u002F\u002Fportswigger.net\u002Fweb-security)\n\n---\n\n**기억하세요**: 보안은 선택 사항이 아닙니다. 하나의 취약점이 전체 플랫폼을 침해할 수 있습니다. 의심스러울 때는 보수적으로 대응하세요.\n","# 보안 리뷰 스킬\n\n이 스킬은 모든 코드가 보안 모범 사례를 따르고 잠재적 취약점을 식별하도록 보장합니다.\n\n## 활성화 시점\n\n- 인증 또는 권한 부여 구현 시\n- 사용자 입력 또는 파일 업로드 처리 시\n- 새로운 API 엔드포인트 생성 시\n- 시크릿 또는 자격 증명 작업 시\n- 결제 기능 구현 시\n- 민감한 데이터 저장 또는 전송 시\n- 서드파티 API 통합 시\n\n## 보안 체크리스트\n\n### 1. 시크릿 관리\n\n#### 절대 하지 말아야 할 것\n```typescript\nconst apiKey = \"sk-proj-xxxxx\"  \u002F\u002F Hardcoded secret\nconst dbPassword = \"password123\" \u002F\u002F In source code\n```\n\n#### 반드시 해야 할 것\n```typescript\nconst apiKey = process.env.OPENAI_API_KEY\nconst dbUrl = process.env.DATABASE_URL\n\n\u002F\u002F Verify secrets exist\nif (!apiKey) {\n  throw new Error('OPENAI_API_KEY not configured')\n}\n```\n\n#### 확인 단계\n- [ ] 하드코딩된 API 키, 토큰, 비밀번호 없음\n- [ ] 모든 시크릿이 환경 변수에 저장됨\n- [ ] `.env.local`이 .gitignore에 포함됨\n- [ ] git 히스토리에 시크릿 없음\n- [ ] 프로덕션 시크릿이 호스팅 플랫폼(Vercel, Railway)에 저장됨\n\n### 2. 입력 유효성 검사\n\n#### 항상 사용자 입력을 검증할 것\n```typescript\nimport { z } from 'zod'\n\n\u002F\u002F Define validation schema\nconst CreateUserSchema = z.object({\n  email: z.string().email(),\n  name: z.string().min(1).max(100),\n  age: z.number().int().min(0).max(150)\n})\n\n\u002F\u002F Validate before processing\nexport async function createUser(input: unknown) {\n  try {\n    const validated = CreateUserSchema.parse(input)\n    return await db.users.create(validated)\n  } catch (error) {\n    if (error instanceof z.ZodError) {\n      return { success: false, errors: error.errors }\n    }\n    throw error\n  }\n}\n```\n\n#### 파일 업로드 유효성 검사\n```typescript\nfunction validateFileUpload(file: File) {\n  \u002F\u002F Size check (5MB max)\n  const maxSize = 5 * 1024 * 1024\n  if (file.size > maxSize) {\n    throw new Error('File too large (max 5MB)')\n  }\n\n  \u002F\u002F Type check\n  const allowedTypes = ['image\u002Fjpeg', 'image\u002Fpng', 'image\u002Fgif']\n  if (!allowedTypes.includes(file.type)) {\n    throw new Error('Invalid file type')\n  }\n\n  \u002F\u002F Extension check\n  const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']\n  const extension = file.name.toLowerCase().match(\u002F\\.[^.]+$\u002F)?.[0]\n  if (!extension || !allowedExtensions.includes(extension)) {\n    throw new Error('Invalid file extension')\n  }\n\n  return true\n}\n```\n\n#### 확인 단계\n- [ ] 모든 사용자 입력이 스키마로 검증됨\n- [ ] 파일 업로드가 제한됨 (크기, 타입, 확장자)\n- [ ] 사용자 입력이 쿼리에 직접 사용되지 않음\n- [ ] 화이트리스트 검증 사용 (블랙리스트가 아닌)\n- [ ] 에러 메시지가 민감한 정보를 노출하지 않음\n\n### 3. SQL Injection 방지\n\n#### 절대 SQL을 연결하지 말 것\n```typescript\n\u002F\u002F DANGEROUS - SQL Injection vulnerability\nconst query = `SELECT * FROM users WHERE email = '${userEmail}'`\nawait db.query(query)\n```\n\n#### 반드시 파라미터화된 쿼리를 사용할 것\n```typescript\n\u002F\u002F Safe - parameterized query\nconst { data } = await supabase\n  .from('users')\n  .select('*')\n  .eq('email', userEmail)\n\n\u002F\u002F Or with raw SQL\nawait db.query(\n  'SELECT * FROM users WHERE email = $1',\n  [userEmail]\n)\n```\n\n#### 확인 단계\n- [ ] 모든 데이터베이스 쿼리가 파라미터화된 쿼리 사용\n- [ ] SQL에서 문자열 연결 없음\n- [ ] ORM\u002F쿼리 빌더가 올바르게 사용됨\n- [ ] Supabase 쿼리가 적절히 새니타이징됨\n\n### 4. 인증 및 권한 부여\n\n#### JWT 토큰 처리\n```typescript\n\u002F\u002F FAIL: WRONG: localStorage (vulnerable to XSS)\nlocalStorage.setItem('token', token)\n\n\u002F\u002F PASS: CORRECT: httpOnly cookies\nres.setHeader('Set-Cookie',\n  `token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)\n```\n\n#### 권한 부여 확인\n```typescript\nexport async function deleteUser(userId: string, requesterId: string) {\n  \u002F\u002F ALWAYS verify authorization first\n  const requester = await db.users.findUnique({\n    where: { id: requesterId }\n  })\n\n  if (requester.role !== 'admin') {\n    return NextResponse.json(\n      { error: 'Unauthorized' },\n      { status: 403 }\n    )\n  }\n\n  \u002F\u002F Proceed with deletion\n  await db.users.delete({ where: { id: userId } })\n}\n```\n\n#### Row Level Security (Supabase)\n```sql\n-- Enable RLS on all tables\nALTER TABLE users ENABLE ROW LEVEL SECURITY;\n\n-- Users can only view their own data\nCREATE POLICY \"Users view own data\"\n  ON users FOR SELECT\n  USING (auth.uid() = id);\n\n-- Users can only update their own data\nCREATE POLICY \"Users update own data\"\n  ON users FOR UPDATE\n  USING (auth.uid() = id);\n```\n\n#### 확인 단계\n- [ ] 토큰이 httpOnly 쿠키에 저장됨 (localStorage가 아닌)\n- [ ] 민감한 작업 전에 권한 부여 확인\n- [ ] Supabase에서 Row Level Security 활성화됨\n- [ ] 역할 기반 접근 제어 구현됨\n- [ ] 세션 관리가 안전함\n\n### 5. XSS 방지\n\n#### HTML 새니타이징\n```typescript\nimport DOMPurify from 'isomorphic-dompurify'\n\n\u002F\u002F ALWAYS sanitize user-provided HTML\nfunction renderUserContent(html: string) {\n  const clean = DOMPurify.sanitize(html, {\n    ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],\n    ALLOWED_ATTR: []\n  })\n  return \u003Cdiv dangerouslySetInnerHTML={{ __html: clean }} \u002F>\n}\n```\n\n#### Content Security Policy\n```typescript\n\u002F\u002F next.config.js\nconst securityHeaders = [\n  {\n    key: 'Content-Security-Policy',\n    value: `\n      default-src 'self';\n      script-src 'self' 'nonce-{nonce}';\n      style-src 'self' 'nonce-{nonce}';\n      img-src 'self' data: https:;\n      font-src 'self';\n      connect-src 'self' https:\u002F\u002Fapi.example.com;\n    `.replace(\u002F\\s{2,}\u002Fg, ' ').trim()\n  }\n]\n```\n\n`{nonce}`는 요청마다 새로 생성하고, 헤더와 인라인 `\u003Cscript>`\u002F`\u003Cstyle>` 태그에 동일하게 주입해야 합니다.\n\n#### 확인 단계\n- [ ] 사용자 제공 HTML이 새니타이징됨\n- [ ] CSP 헤더가 구성됨\n- [ ] 검증되지 않은 동적 콘텐츠 렌더링 없음\n- [ ] React의 내장 XSS 보호가 사용됨\n\n### 6. CSRF 보호\n\n#### CSRF 토큰\n```typescript\nimport { csrf } from '@\u002Flib\u002Fcsrf'\n\nexport async function POST(request: Request) {\n  const token = request.headers.get('X-CSRF-Token')\n\n  if (!csrf.verify(token)) {\n    return NextResponse.json(\n      { error: 'Invalid CSRF token' },\n      { status: 403 }\n    )\n  }\n\n  \u002F\u002F Process request\n}\n```\n\n#### SameSite 쿠키\n```typescript\nres.setHeader('Set-Cookie',\n  `session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)\n```\n\n#### 확인 단계\n- [ ] 상태 변경 작업에 CSRF 토큰 적용\n- [ ] 모든 쿠키에 SameSite=Strict 설정\n- [ ] Double-submit 쿠키 패턴 구현\n\n### 7. 속도 제한\n\n#### API 속도 제한\n```typescript\nimport rateLimit from 'express-rate-limit'\n\nconst limiter = rateLimit({\n  windowMs: 15 * 60 * 1000, \u002F\u002F 15 minutes\n  max: 100, \u002F\u002F 100 requests per window\n  message: 'Too many requests'\n})\n\n\u002F\u002F Apply to routes\napp.use('\u002Fapi\u002F', limiter)\n```\n\n#### 비용이 높은 작업\n```typescript\n\u002F\u002F Aggressive rate limiting for searches\nconst searchLimiter = rateLimit({\n  windowMs: 60 * 1000, \u002F\u002F 1 minute\n  max: 10, \u002F\u002F 10 requests per minute\n  message: 'Too many search requests'\n})\n\napp.use('\u002Fapi\u002Fsearch', searchLimiter)\n```\n\n#### 확인 단계\n- [ ] 모든 API 엔드포인트에 속도 제한 적용\n- [ ] 비용이 높은 작업에 더 엄격한 제한\n- [ ] IP 기반 속도 제한\n- [ ] 사용자 기반 속도 제한 (인증된 사용자)\n\n### 8. 민감한 데이터 노출\n\n#### 로깅\n```typescript\n\u002F\u002F FAIL: WRONG: Logging sensitive data\nconsole.log('User login:', { email, password })\nconsole.log('Payment:', { cardNumber, cvv })\n\n\u002F\u002F PASS: CORRECT: Redact sensitive data\nconsole.log('User login:', { email, userId })\nconsole.log('Payment:', { last4: card.last4, userId })\n```\n\n#### 에러 메시지\n```typescript\n\u002F\u002F FAIL: WRONG: Exposing internal details\ncatch (error) {\n  return NextResponse.json(\n    { error: error.message, stack: error.stack },\n    { status: 500 }\n  )\n}\n\n\u002F\u002F PASS: CORRECT: Generic error messages\ncatch (error) {\n  console.error('Internal error:', error)\n  return NextResponse.json(\n    { error: 'An error occurred. Please try again.' },\n    { status: 500 }\n  )\n}\n```\n\n#### 확인 단계\n- [ ] 로그에 비밀번호, 토큰, 시크릿 없음\n- [ ] 사용자에게 표시되는 에러 메시지가 일반적임\n- [ ] 상세 에러는 서버 로그에만 기록\n- [ ] 사용자에게 스택 트레이스가 노출되지 않음\n\n### 9. 블록체인 보안 (Solana)\n\n#### 지갑 검증\n```typescript\nimport nacl from 'tweetnacl'\nimport bs58 from 'bs58'\nimport { PublicKey } from '@solana\u002Fweb3.js'\n\nasync function verifyWalletOwnership(\n  publicKey: string,\n  signature: string,\n  message: string\n) {\n  try {\n    const publicKeyBytes = new PublicKey(publicKey).toBytes()\n    const signatureBytes = bs58.decode(signature)\n    const messageBytes = new TextEncoder().encode(message)\n\n    return nacl.sign.detached.verify(\n      messageBytes,\n      signatureBytes,\n      publicKeyBytes\n    )\n  } catch (error) {\n    return false\n  }\n}\n```\n\n참고: Solana 공개 키와 서명은 일반적으로 base64가 아니라 base58로 인코딩됩니다.\n\n#### 트랜잭션 검증\n```typescript\nasync function verifyTransaction(transaction: Transaction) {\n  \u002F\u002F Verify recipient\n  if (transaction.to !== expectedRecipient) {\n    throw new Error('Invalid recipient')\n  }\n\n  \u002F\u002F Verify amount\n  if (transaction.amount > maxAmount) {\n    throw new Error('Amount exceeds limit')\n  }\n\n  \u002F\u002F Verify user has sufficient balance\n  const balance = await getBalance(transaction.from)\n  if (balance \u003C transaction.amount) {\n    throw new Error('Insufficient balance')\n  }\n\n  return true\n}\n```\n\n#### 확인 단계\n- [ ] 지갑 서명 검증됨\n- [ ] 트랜잭션 세부 정보 유효성 검사됨\n- [ ] 트랜잭션 전 잔액 확인\n- [ ] 블라인드 트랜잭션 서명 없음\n\n### 10. 의존성 보안\n\n#### 정기 업데이트\n```bash\n# Check for vulnerabilities\nnpm audit\n\n# Fix automatically fixable issues\nnpm audit fix\n\n# Update dependencies\nnpm update\n\n# Check for outdated packages\nnpm outdated\n```\n\n#### 잠금 파일\n```bash\n# ALWAYS commit lock files\ngit add package-lock.json\n\n# Use in CI\u002FCD for reproducible builds\nnpm ci  # Instead of npm install\n```\n\n#### 확인 단계\n- [ ] 의존성이 최신 상태\n- [ ] 알려진 취약점 없음 (npm audit 클린)\n- [ ] 잠금 파일 커밋됨\n- [ ] GitHub에서 Dependabot 활성화됨\n- [ ] 정기적인 보안 업데이트\n\n## 보안 테스트\n\n### 자동화된 보안 테스트\n```typescript\n\u002F\u002F Test authentication\ntest('requires authentication', async () => {\n  const response = await fetch('\u002Fapi\u002Fprotected')\n  expect(response.status).toBe(401)\n})\n\n\u002F\u002F Test authorization\ntest('requires admin role', async () => {\n  const response = await fetch('\u002Fapi\u002Fadmin', {\n    headers: { Authorization: `Bearer ${userToken}` }\n  })\n  expect(response.status).toBe(403)\n})\n\n\u002F\u002F Test input validation\ntest('rejects invalid input', async () => {\n  const response = await fetch('\u002Fapi\u002Fusers', {\n    method: 'POST',\n    body: JSON.stringify({ email: 'not-an-email' })\n  })\n  expect(response.status).toBe(400)\n})\n\n\u002F\u002F Test rate limiting\ntest('enforces rate limits', async () => {\n  const requests = Array(101).fill(null).map(() =>\n    fetch('\u002Fapi\u002Fendpoint')\n  )\n\n  const responses = await Promise.all(requests)\n  const tooManyRequests = responses.filter(r => r.status === 429)\n\n  expect(tooManyRequests.length).toBeGreaterThan(0)\n})\n```\n\n## 배포 전 보안 체크리스트\n\n모든 프로덕션 배포 전:\n\n- [ ] **시크릿**: 하드코딩된 시크릿 없음, 모두 환경 변수에 저장\n- [ ] **입력 유효성 검사**: 모든 사용자 입력 검증됨\n- [ ] **SQL Injection**: 모든 쿼리 파라미터화됨\n- [ ] **XSS**: 사용자 콘텐츠 새니타이징됨\n- [ ] **CSRF**: 보호 활성화됨\n- [ ] **인증**: 적절한 토큰 처리\n- [ ] **권한 부여**: 역할 확인 적용됨\n- [ ] **속도 제한**: 모든 엔드포인트에서 활성화됨\n- [ ] **HTTPS**: 프로덕션에서 강제 적용\n- [ ] **보안 헤더**: CSP, X-Frame-Options 구성됨\n- [ ] **에러 처리**: 에러에 민감한 데이터 없음\n- [ ] **로깅**: 민감한 데이터가 로그에 없음\n- [ ] **의존성**: 최신 상태, 취약점 없음\n- [ ] **Row Level Security**: Supabase에서 활성화됨\n- [ ] **CORS**: 적절히 구성됨\n- [ ] **파일 업로드**: 유효성 검사됨 (크기, 타입)\n- [ ] **지갑 서명**: 검증됨 (블록체인인 경우)\n\n## 참고 자료\n\n- [OWASP Top 10](https:\u002F\u002Fowasp.org\u002Fwww-project-top-ten\u002F)\n- [Next.js Security](https:\u002F\u002Fnextjs.org\u002Fdocs\u002Fsecurity)\n- [Supabase Security](https:\u002F\u002Fsupabase.com\u002Fdocs\u002Fguides\u002Fauth)\n- [Web Security Academy](https:\u002F\u002Fportswigger.net\u002Fweb-security)\n\n---\n\n**기억하세요**: 보안은 선택 사항이 아닙니다. 하나의 취약점이 전체 플랫폼을 침해할 수 있습니다. 의심스러울 때는 보수적으로 대응하세요.\n",{"name":5,"origin":24,"description":7},"ECC","main","87afe6ce96220accf50a345dea46784c8c9f210e","YYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fko-KR\u002Fskills\u002Fsecurity-review",0,"https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base","https:\u002F\u002Fkb.yyc3.top\u002F","https:\u002F\u002Fgithub.com\u002FYYC-Cube","万象归元于云枢 | 深栈智启新纪元","2026-11-10 10:37:32",{"source":8,"skillName":5,"commands":35,"prompt":45,"zipAvailable":46},[36,39,42],{"tool":37,"command":38},"npx","npx skills add YYC-Cube\u002FYYC3-Knowledge-Base --skill security-review",{"tool":40,"command":41},"bunx","bunx skills add YYC-Cube\u002FYYC3-Knowledge-Base --skill security-review",{"tool":43,"command":44},"pnpm","pnpm dlx skills add YYC-Cube\u002FYYC3-Knowledge-Base --skill security-review","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fko-KR\u002Fskills\u002Fsecurity-review\u002FSKILL.md",true,[48],{"path":49,"resourceType":50,"resourceTypeDesc":51,"sizeBytes":52,"sourceHtmlUrl":53},"cloud-infrastructure-security.md","REFERENCE","参考文档",10619,"https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-AI-应用工具\u002FYYC3-AI-应用工具-Everything-claude-code\u002Fdocs\u002Fko-KR\u002Fskills\u002Fsecurity-review\u002Fcloud-infrastructure-security.md",[55,62,69,76,83,90],{"skillNo":56,"name":57,"nameZh":6,"description":58,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":59,"categories":60,"keywords":61,"publishedAt":19},"01M7CTDWHERCXPQCP0K03A86AS","vercel-sandbox","Run agent-browser + Chrome inside Vercel Sandbox microVMs for browser automation from any Vercel-deployed app. Use when the user needs browser automation in a Vercel app (Next.js, SvelteKit, Nuxt, Remix, Astro, etc.), wants to run headless Chrome without binary size limits, needs persistent browser sessions across commands, or wants ephemeral isolated browser environments. Triggers include \"Vercel Sandbox browser\", \"microVM Chrome\", \"agent-browser in sandbox\", \"browser automation on Vercel\", or any task requiring Chrome in a Vercel Sandbox.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fvercel-sandbox\u002FSKILL.md",[],[],{"skillNo":63,"name":64,"nameZh":6,"description":65,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":66,"categories":67,"keywords":68,"publishedAt":19},"01M7CTD8D2BNJY54S65M47HEJA","slack","Interact with Slack workspaces using browser automation. Use when the user needs to check unread channels, navigate Slack, send messages, extract data, find information, search conversations, or automate any Slack task. Triggers include \"check my Slack\", \"what channels have unreads\", \"send a message to\", \"search Slack for\", \"extract from Slack\", \"find who said\", or any task requiring programmatic Slack interaction.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fslack\u002FSKILL.md",[],[],{"skillNo":70,"name":71,"nameZh":6,"description":72,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":73,"categories":74,"keywords":75,"publishedAt":19},"01M7CTCSSB8ZP3Q58C3YZDZK1W","electron","Automate Electron desktop apps (VS Code, Slack, Discord, Figma, Notion, Spotify, etc.) using agent-browser via Chrome DevTools Protocol. Use when the user needs to interact with an Electron app, automate a desktop app, connect to a running app, control a native app, or test an Electron application. Triggers include \"automate Slack app\", \"control VS Code\", \"interact with Discord app\", \"test this Electron app\", \"connect to desktop app\", or any task requiring automation of a native Electron application.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Felectron\u002FSKILL.md",[],[],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":82,"publishedAt":19},"01M7CTCBJY76688V4DCZGWFHGQ","dogfood","Systematically explore and test a web application to find bugs, UX issues, and other problems. Use when asked to \"dogfood\", \"QA\", \"exploratory test\", \"find issues\", \"bug hunt\", \"test this app\u002Fsite\u002Fplatform\", or review the quality of a web application. Produces a structured report with full reproduction evidence -- step-by-step screenshots, repro videos, and detailed repro steps for every issue -- so findings can be handed directly to the responsible teams.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fdogfood\u002FSKILL.md",[],[],{"skillNo":84,"name":85,"nameZh":6,"description":86,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":87,"categories":88,"keywords":89,"publishedAt":19},"01M7CTC4H0FFGVEAQR9PA4GA1A","agent-browser","Browser automation CLI for AI agents. Use when the user needs to interact with websites, including navigating pages, filling forms, clicking buttons, taking screenshots, extracting data, testing web apps, or automating any browser task. Triggers include requests to \"open a website\", \"fill out a form\", \"click a button\", \"take a screenshot\", \"scrape data from a page\", \"test this web app\", \"login to a site\", \"automate browser actions\", or any task requiring programmatic web interaction.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-WebSpec-Web标准\u002FYYC3-WebSpec-Web标准-standards\u002Fbrowser-agent\u002Fskills\u002Fagent-browser\u002FSKILL.md",[],[],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":96,"publishedAt":19},"01M7CTBKVFYBG78FDSABSDJTTQ","shadcn","Manages shadcn components and projects — adding, searching, fixing, debugging, styling, and composing UI, including chat interfaces. Provides project context, component docs, and usage examples. Applies when working with shadcn\u002Fui, component registries, presets, --preset codes, or any project with a components.json file. Also triggers for \"shadcn init\", \"create an app with --preset\", \"switch to --preset\", and for building a design system or applying a DESIGN.md or brand spec to shadcn\u002Fui.","https:\u002F\u002Fgithub.com\u002FYYC-Cube\u002FYYC3-Knowledge-Base\u002Fblob\u002Fmain\u002FYYC3-UI-界面组件\u002FYYC3-UI-界面组件-ui\u002Fskills\u002Fshadcn\u002FSKILL.md",[],[]]