security-review
인증 추가, 사용자 입력 처리, 시크릿 다루기, API 엔드포인트 생성, 결제·민감 기능 구현 시 사용한다. 종합 보안 체크리스트와 패턴을 제공한다.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
القائمة
인증 추가, 사용자 입력 처리, 시크릿 다루기, API 엔드포인트 생성, 결제·민감 기능 구현 시 사용한다. 종합 보안 체크리스트와 패턴을 제공한다.
التثبيت باستخدام Codex أو Claude انسخ هذا Prompt والصقه في Codex أو Claude أو مساعد آخر ليراجع صفحة Skill ويثبّتها لك.
استنادا إلى تصنيف SOC المهني
새 Spring Boot 서비스를 api-gateway + auth-api 에코시스템에 연결한다. auth-api 클라이언트 등록 → Gateway 라우팅 추가 → 서비스에 econo-passport 연동 → 동작 확인까지 한 번에 처리. 다음 상황에서 반드시 이 스킬을 사용한다: - "새 서비스 Gateway에 연결해줘", "새 서비스 auth 연동" - "서비스 등록해줘", "Gateway 뒤에 붙여줘" - "/register-service" 직접 호출 - 새 Spring Boot 서비스가 추가되고 인증이 필요할 때 ARGUMENTS: 서비스명, 서비스 경로(선택), 업스트림 URL(선택) 예: "EEOS-BE /Users/mando/study/eeos/EEOS-BE/eeos"
이 스킬은 사용자가 "PR 생성해줘", "PR 만들어줘", "pull request 생성", "pr 올려줘", "/git-pr" 등을 요청할 때 호출된다. 현재 브랜치의 커밋을 원격으로 push하고, `.github/PULL_REQUEST_TEMPLATE.md`를 채워 GitHub Pull Request를 생성한다.
신규 기능을 개발할 때 정책 문서 → 코드 → 테스트 순서로 작성한다. ADR은 기술적 결정에만 사용하고, 기능 정책은 docs/features/ 에 작성한다. 다음 상황에서 반드시 이 스킬을 사용한다: - "기능 추가해줘", "기획부터 해봐", "설계해봐" - 새로운 API 엔드포인트 또는 도메인 규칙이 생길 때 - "/new-feature" 직접 호출 ARGUMENTS: 기능 이름 또는 요구사항 (없으면 대화에서 추출)
Architecture Decision Record(ADR)를 작성한다. 기술적 결정사항, 설계 선택, 트레이드오프를 문서화하여 나중에 "왜 이렇게 했지?"를 알 수 있게 한다. 다음 상황에서 반드시 이 스킬을 사용한다: - "ADR 써줘", "결정사항 문서화해줘", "이 결정 기록해줘" - 기술 방향 선택 후 ("A 대신 B 쓰기로 했어") - 설계 논의가 끝났을 때 - 나중에 이 결정이 왜 내려졌는지 설명이 필요할 것 같을 때 - "/adr" 직접 호출 ARGUMENTS: 결정 내용 또는 결정 번호 (없으면 대화에서 추출)
AI 에이전트의 액션 스페이스, 도구 정의, 관측(Observation) 포맷을 설계·최적화해 작업 완수율을 높일 때 사용한다.
로컬 개발, 컨테이너 보안, 네트워킹, 볼륨 전략, 멀티 서비스 오케스트레이션을 위한 Docker 및 Docker Compose 패턴.
| name | security-review |
| description | 인증 추가, 사용자 입력 처리, 시크릿 다루기, API 엔드포인트 생성, 결제·민감 기능 구현 시 사용한다. 종합 보안 체크리스트와 패턴을 제공한다. |
| origin | ECC |
이 스킬은 모든 코드가 보안 베스트 프랙티스를 따르고 잠재적 취약점을 식별하도록 보장한다.
const apiKey = "sk-proj-xxxxx" // 하드코딩된 시크릿
const dbPassword = "password123" // 소스 코드에 포함
const apiKey = process.env.OPENAI_API_KEY
const dbUrl = process.env.DATABASE_URL
// 시크릿 존재 여부 검증
if (!apiKey) {
throw new Error('OPENAI_API_KEY not configured')
}
.env.local이 .gitignore에 포함됨import { z } from 'zod'
// 검증 스키마 정의
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
age: z.number().int().min(0).max(150)
})
// 처리 전에 검증
export async function createUser(input: unknown) {
try {
const validated = CreateUserSchema.parse(input)
return await db.users.create(validated)
} catch (error) {
if (error instanceof z.ZodError) {
return { success: false, errors: error.errors }
}
throw error
}
}
function validateFileUpload(file: File) {
// 크기 검사 (최대 5MB)
const maxSize = 5 * 1024 * 1024
if (file.size > maxSize) {
throw new Error('File too large (max 5MB)')
}
// 타입 검사
const allowedTypes = ['image/jpeg', 'image/png', 'image/gif']
if (!allowedTypes.includes(file.type)) {
throw new Error('Invalid file type')
}
// 확장자 검사
const allowedExtensions = ['.jpg', '.jpeg', '.png', '.gif']
const extension = file.name.toLowerCase().match(/\.[^.]+$/)?.[0]
if (!extension || !allowedExtensions.includes(extension)) {
throw new Error('Invalid file extension')
}
return true
}
// 위험 - SQL 인젝션 취약점
const query = `SELECT * FROM users WHERE email = '${userEmail}'`
await db.query(query)
// 안전 - 파라미터화된 쿼리
const { data } = await supabase
.from('users')
.select('*')
.eq('email', userEmail)
// 또는 raw SQL로
await db.query(
'SELECT * FROM users WHERE email = $1',
[userEmail]
)
// FAIL: 잘못됨: localStorage (XSS에 취약)
localStorage.setItem('token', token)
// PASS: 올바름: httpOnly 쿠키
res.setHeader('Set-Cookie',
`token=${token}; HttpOnly; Secure; SameSite=Strict; Max-Age=3600`)
export async function deleteUser(userId: string, requesterId: string) {
// 민감한 작업 전 항상 인가를 검증
const requester = await db.users.findUnique({
where: { id: requesterId }
})
if (requester.role !== 'admin') {
return NextResponse.json(
{ error: 'Unauthorized' },
{ status: 403 }
)
}
// 삭제 진행
await db.users.delete({ where: { id: userId } })
}
-- 모든 테이블에 RLS 활성화
ALTER TABLE users ENABLE ROW LEVEL SECURITY;
-- 사용자는 자기 데이터만 조회 가능
CREATE POLICY "Users view own data"
ON users FOR SELECT
USING (auth.uid() = id);
-- 사용자는 자기 데이터만 수정 가능
CREATE POLICY "Users update own data"
ON users FOR UPDATE
USING (auth.uid() = id);
import DOMPurify from 'isomorphic-dompurify'
// 사용자 제공 HTML은 항상 새니타이즈
function renderUserContent(html: string) {
const clean = DOMPurify.sanitize(html, {
ALLOWED_TAGS: ['b', 'i', 'em', 'strong', 'p'],
ALLOWED_ATTR: []
})
return <div dangerouslySetInnerHTML={{ __html: clean }} />
}
// next.config.js
const securityHeaders = [
{
key: 'Content-Security-Policy',
value: `
default-src 'self';
script-src 'self' 'unsafe-eval' 'unsafe-inline';
style-src 'self' 'unsafe-inline';
img-src 'self' data: https:;
font-src 'self';
connect-src 'self' https://api.example.com;
`.replace(/\s{2,}/g, ' ').trim()
}
]
import { csrf } from '@/lib/csrf'
export async function POST(request: Request) {
const token = request.headers.get('X-CSRF-Token')
if (!csrf.verify(token)) {
return NextResponse.json(
{ error: 'Invalid CSRF token' },
{ status: 403 }
)
}
// 요청 처리
}
res.setHeader('Set-Cookie',
`session=${sessionId}; HttpOnly; Secure; SameSite=Strict`)
import rateLimit from 'express-rate-limit'
const limiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15분
max: 100, // 윈도우당 100 요청
message: 'Too many requests'
})
// 라우트에 적용
app.use('/api/', limiter)
// 검색에는 더 강한 레이트 리미팅
const searchLimiter = rateLimit({
windowMs: 60 * 1000, // 1분
max: 10, // 분당 10 요청
message: 'Too many search requests'
})
app.use('/api/search', searchLimiter)
// FAIL: 잘못됨: 민감 데이터 로깅
console.log('User login:', { email, password })
console.log('Payment:', { cardNumber, cvv })
// PASS: 올바름: 민감 데이터 마스킹
console.log('User login:', { email, userId })
console.log('Payment:', { last4: card.last4, userId })
// FAIL: 잘못됨: 내부 세부사항 노출
catch (error) {
return NextResponse.json(
{ error: error.message, stack: error.stack },
{ status: 500 }
)
}
// PASS: 올바름: 일반 에러 메시지
catch (error) {
console.error('Internal error:', error)
return NextResponse.json(
{ error: 'An error occurred. Please try again.' },
{ status: 500 }
)
}
import { verify } from '@solana/web3.js'
async function verifyWalletOwnership(
publicKey: string,
signature: string,
message: string
) {
try {
const isValid = verify(
Buffer.from(message),
Buffer.from(signature, 'base64'),
Buffer.from(publicKey, 'base64')
)
return isValid
} catch (error) {
return false
}
}
async function verifyTransaction(transaction: Transaction) {
// 수신자 검증
if (transaction.to !== expectedRecipient) {
throw new Error('Invalid recipient')
}
// 금액 검증
if (transaction.amount > maxAmount) {
throw new Error('Amount exceeds limit')
}
// 사용자가 충분한 잔액을 가졌는지 검증
const balance = await getBalance(transaction.from)
if (balance < transaction.amount) {
throw new Error('Insufficient balance')
}
return true
}
# 취약점 확인
npm audit
# 자동 수정 가능한 이슈 수정
npm audit fix
# 의존성 업데이트
npm update
# 오래된 패키지 확인
npm outdated
# 항상 lock 파일을 커밋
git add package-lock.json
# 재현 가능한 빌드를 위해 CI/CD에서 사용
npm ci # npm install 대신
// 인증 테스트
test('requires authentication', async () => {
const response = await fetch('/api/protected')
expect(response.status).toBe(401)
})
// 인가 테스트
test('requires admin role', async () => {
const response = await fetch('/api/admin', {
headers: { Authorization: `Bearer ${userToken}` }
})
expect(response.status).toBe(403)
})
// 입력 검증 테스트
test('rejects invalid input', async () => {
const response = await fetch('/api/users', {
method: 'POST',
body: JSON.stringify({ email: 'not-an-email' })
})
expect(response.status).toBe(400)
})
// 레이트 리미팅 테스트
test('enforces rate limits', async () => {
const requests = Array(101).fill(null).map(() =>
fetch('/api/endpoint')
)
const responses = await Promise.all(requests)
const tooManyRequests = responses.filter(r => r.status === 429)
expect(tooManyRequests.length).toBeGreaterThan(0)
})
운영 배포 전에 반드시 확인한다:
기억할 것: 보안은 선택이 아니다. 단 하나의 취약점이 전체 플랫폼을 손상시킬 수 있다. 의심스러우면 보수적으로 판단한다.