AgentSkillsCN

security-first

安全至上开发指南——编写不易被黑客攻击的代码。

SKILL.md
--- frontmatter
name: security-first
description: 보안 최우선 개발 가이드 - 해킹당하지 않는 코드 작성하기

보안 최우선 개발 가이드

핵심 원칙

"모든 입력은 악의적이고, 모든 사용자는 해커다"

1. 신뢰하지 말고 검증하라 (Never Trust, Always Verify)

  • 클라이언트에서 온 데이터는 모두 검증
  • 서버는 항상 진실의 원천

2. 최소 권한 원칙 (Principle of Least Privilege)

  • 필요한 최소한의 권한만 부여
  • 기본은 "거부", 명시적으로만 "허용"

3. 심층 방어 (Defense in Depth)

  • 여러 겹의 보안 장치
  • 하나가 뚫려도 다른 것이 막음

위협 순위 (한국 서비스 기준)

🔴 즉시 대응 필요 (Critical)

typescript
// 1. SQL Injection
// ❌ 절대 금지
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;

// ✅ Prepared Statement 사용
const query = 'SELECT * FROM users WHERE id = ?';
db.query(query, [req.params.id]);

// 2. XSS (Cross-Site Scripting)
// ❌ 절대 금지
div.innerHTML = userInput;

// ✅ 이스케이프 처리
div.textContent = userInput;
// 또는 DOMPurify 사용
div.innerHTML = DOMPurify.sanitize(userInput);

// 3. 인증 토큰 노출
// ❌ 절대 금지
localStorage.setItem('token', jwt);  // XSS에 취약

// ✅ HttpOnly 쿠키 사용
res.cookie('token', jwt, {
  httpOnly: true,
  secure: true,
  sameSite: 'strict'
});

🟠 반드시 구현 (High)

typescript
// 4. CSRF 방어
// ✅ CSRF 토큰 검증
app.use(csrf());

// 5. Rate Limiting
// ✅ 무차별 대입 공격 방지
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 100 // 최대 100번
});
app.use('/api/', limiter);

// 6. HTTPS 강제
// ✅ 프로덕션에서 필수
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  }
  next();
});

OWASP Top 10 대응

1. Injection 공격 방어

typescript
// SQL Injection
// ❌ 위험한 코드
const username = req.body.username;
db.query(`SELECT * FROM users WHERE username = '${username}'`);

// ✅ 안전한 코드 (ORM 사용)
const user = await prisma.user.findUnique({
  where: { username: req.body.username }
});

// NoSQL Injection
// ❌ 위험한 코드
User.findOne({ username: req.body.username });
// 공격: { "username": { "$ne": null } }

// ✅ 안전한 코드 (타입 검증)
const username = String(req.body.username);
User.findOne({ username });

2. Broken Authentication

typescript
// ❌ 약한 인증
// - 비밀번호 평문 저장
// - 세션 타임아웃 없음
// - 약한 비밀번호 허용

// ✅ 강한 인증
import bcrypt from 'bcrypt';
import jwt from 'jsonwebtoken';

// 비밀번호 해싱
const hashedPassword = await bcrypt.hash(password, 10);

// JWT 생성 (짧은 만료 시간)
const token = jwt.sign(
  { userId: user.id },
  process.env.JWT_SECRET!,
  { expiresIn: '1h' }
);

// 비밀번호 복잡도 검증
const passwordRegex = /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/;
if (!passwordRegex.test(password)) {
  throw new Error('비밀번호는 8자 이상, 대소문자, 숫자, 특수문자 포함');
}

3. Sensitive Data Exposure

typescript
// ❌ 민감 정보 노출
res.json({
  user: {
    id: 1,
    email: 'user@example.com',
    password: hashedPassword,  // ❌
    ssn: '123456-1234567',     // ❌
    creditCard: '1234-5678'    // ❌
  }
});

// ✅ 필요한 정보만 전송
res.json({
  user: {
    id: 1,
    email: 'user@example.com',
    name: '홍길동'
  }
});

// ✅ 민감 정보 마스킹
const maskedSSN = ssn.replace(/^(\d{6})-(\d)(\d{6})$/, '$1-$2******');
const maskedCard = cardNumber.replace(/^(\d{4})-(\d{4})-(\d{4})-(\d{4})$/, '$1-****-****-$4');

4. XML External Entities (XXE)

typescript
// ❌ 위험한 XML 파싱
const xml2js = require('xml2js');
xml2js.parseString(userInput, callback);

// ✅ 안전한 설정
const xml2js = require('xml2js');
const parser = new xml2js.Parser({
  explicitRoot: false,
  explicitArray: false,
  ignoreAttrs: true
});

5. Broken Access Control

typescript
// ❌ 권한 검증 없음
app.get('/api/users/:id/orders', async (req, res) => {
  const orders = await getOrders(req.params.id);
  res.json(orders);
  // 다른 사용자의 주문도 볼 수 있음!
});

// ✅ 권한 검증
app.get('/api/users/:id/orders', requireAuth, async (req, res) => {
  // 본인 또는 관리자만 조회 가능
  if (req.user.id !== req.params.id && !req.user.isAdmin) {
    return res.status(403).json({ error: '권한이 없습니다' });
  }

  const orders = await getOrders(req.params.id);
  res.json(orders);
});

6. Security Misconfiguration

typescript
// ❌ 위험한 설정
const express = require('express');
const app = express();

// ✅ 안전한 설정
import helmet from 'helmet';
import cors from 'cors';

app.use(helmet()); // 보안 헤더 자동 설정

app.use(cors({
  origin: process.env.ALLOWED_ORIGINS?.split(','),
  credentials: true
}));

// 에러 메시지에서 민감 정보 제거
app.use((err, req, res, next) => {
  console.error(err.stack); // 서버 로그만

  res.status(500).json({
    error: process.env.NODE_ENV === 'production'
      ? '서버 오류가 발생했습니다'
      : err.message  // 개발 환경에서만 상세 메시지
  });
});

7. Cross-Site Scripting (XSS)

typescript
// ❌ 위험한 코드
// React
<div dangerouslySetInnerHTML={{ __html: userInput }} />

// Vanilla JS
element.innerHTML = userInput;

// ✅ 안전한 코드
// React (자동 이스케이프)
<div>{userInput}</div>

// DOMPurify 사용 (HTML 필요 시)
import DOMPurify from 'dompurify';
<div dangerouslySetInnerHTML={{
  __html: DOMPurify.sanitize(userInput)
}} />

// Content Security Policy 설정
app.use(helmet.contentSecurityPolicy({
  directives: {
    defaultSrc: ["'self'"],
    scriptSrc: ["'self'", "'unsafe-inline'"],
    styleSrc: ["'self'", "'unsafe-inline'"],
    imgSrc: ["'self'", 'data:', 'https:'],
  }
}));

8. Insecure Deserialization

typescript
// ❌ 위험한 역직렬화
const userData = JSON.parse(req.body.data);
eval(userData.code); // 절대 금지!

// ✅ 안전한 처리
// 1. JSON만 허용
const userData = JSON.parse(req.body.data);

// 2. 스키마 검증
import { z } from 'zod';

const userSchema = z.object({
  name: z.string(),
  email: z.string().email(),
  age: z.number().min(0).max(150)
});

const userData = userSchema.parse(JSON.parse(req.body.data));

9. Using Components with Known Vulnerabilities

bash
# ✅ 정기적으로 업데이트 확인
npm audit
npm audit fix

# package.json에 정확한 버전 명시
{
  "dependencies": {
    "express": "4.18.2",  # ✅ 정확한 버전
    "lodash": "^4.17.21"  # 😐 마이너 업데이트 허용
  }
}

# Dependabot 활성화 (GitHub)
# 자동으로 취약점 알림 + PR 생성

10. Insufficient Logging & Monitoring

typescript
// ✅ 중요한 이벤트 로깅
import winston from 'winston';

const logger = winston.createLogger({
  level: 'info',
  format: winston.format.json(),
  transports: [
    new winston.transports.File({ filename: 'error.log', level: 'error' }),
    new winston.transports.File({ filename: 'combined.log' })
  ]
});

// 로그인 시도
logger.info('Login attempt', {
  email: req.body.email,
  ip: req.ip,
  userAgent: req.headers['user-agent'],
  success: false
});

// 민감한 작업
logger.warn('Password change', {
  userId: req.user.id,
  ip: req.ip,
  timestamp: new Date()
});

// 의심스러운 활동
if (failedAttempts > 5) {
  logger.error('Possible brute force attack', {
    email: req.body.email,
    ip: req.ip,
    attempts: failedAttempts
  });
}

한국 특화 보안

1. 주민등록번호 처리

typescript
// ❌ 절대 금지
// - 평문 저장
// - 로그에 기록
// - URL 파라미터로 전송
// - 클라이언트 저장

// ✅ 안전한 처리
import crypto from 'crypto';

// 암호화 저장
function encryptSSN(ssn: string): string {
  const algorithm = 'aes-256-gcm';
  const key = Buffer.from(process.env.ENCRYPTION_KEY!, 'hex');
  const iv = crypto.randomBytes(16);

  const cipher = crypto.createCipheriv(algorithm, key, iv);
  let encrypted = cipher.update(ssn, 'utf8', 'hex');
  encrypted += cipher.final('hex');

  const authTag = cipher.getAuthTag();

  return `${iv.toString('hex')}:${authTag.toString('hex')}:${encrypted}`;
}

// 마스킹 표시
function maskSSN(ssn: string): string {
  return ssn.replace(/^(\d{6})-(\d)(\d{6})$/, '$1-$2******');
}

// API 응답
res.json({
  user: {
    name: '홍길동',
    ssn: maskSSN(user.ssn),  // 900101-1******
    // 원본은 절대 전송하지 않음
  }
});

2. 개인정보 수집 최소화

typescript
// ❌ 불필요한 정보 수집
interface User {
  name: string;
  ssn: string;           // 정말 필요한가?
  address: string;       // 정말 필요한가?
  phone: string;
  email: string;
  birthdate: Date;       // SSN에서 추출 가능
}

// ✅ 필요한 정보만
interface User {
  name: string;
  phone: string;        // 본인 인증용
  email: string;
  // 주민번호는 본인 인증 시에만 일회성으로 사용
}

3. 본인 인증 연동

typescript
// ✅ NICE/PASS 본인인증 사용
// 주민번호를 직접 수집하지 않고 인증 결과만 받기

interface VerificationResult {
  name: string;
  birthdate: string;    // 생년월일만
  gender: 'M' | 'F';
  ci: string;           // 연계정보 (개인 식별용, 암호화됨)
  di: string;           // 중복가입확인정보
}

// CI/DI는 암호화된 고유값이므로 안전
// 주민번호 원본은 받지 않음

보안 체크리스트

배포 전 필수 확인

markdown
### 인증 & 인가
- [ ] 비밀번호 해싱 (bcrypt, scrypt)
- [ ] JWT 만료 시간 설정 (< 24시간)
- [ ] Refresh Token 구현
- [ ] Rate Limiting 적용
- [ ] HTTPS 강제
- [ ] 쿠키 보안 플래그 (HttpOnly, Secure, SameSite)

### 입력 검증
- [ ] 모든 입력에 타입 검증
- [ ] SQL Injection 방어 (Prepared Statement)
- [ ] XSS 방어 (이스케이프 처리)
- [ ] CSRF 토큰 검증
- [ ] 파일 업로드 검증 (타입, 크기, 확장자)

### 데이터 보호
- [ ] 민감 정보 암호화
- [ ] 민감 정보 로그 제외
- [ ] 민감 정보 URL 제외
- [ ] 에러 메시지에 내부 정보 제외
- [ ] 개인정보 수집 최소화

### 설정
- [ ] 보안 헤더 설정 (Helmet.js)
- [ ] CORS 설정
- [ ] 환경 변수로 비밀키 관리
- [ ] 불필요한 서비스 비활성화
- [ ] 최신 보안 패치 적용

### 모니터링
- [ ] 로그인 시도 로깅
- [ ] 민감한 작업 로깅
- [ ] 에러 로깅
- [ ] 의심스러운 활동 알림

긴급 상황 대응

해킹 의심 시 즉시 행동

bash
# 1. 서비스 점검 모드 전환
# 2. 모든 세션 무효화
# 3. 비밀번호 강제 재설정
# 4. 로그 분석
# 5. 취약점 패치
# 6. 사용자 공지

개인정보 유출 시

markdown
1. 유출 범위 확인
2. 즉시 보안 조치
3. 한국인터넷진흥원(KISA) 신고
4. 개인정보보호위원회 신고
5. 사용자 개별 통지
6. 홈페이지 공지
7. 재발 방지 대책 수립

보안 도구 추천

bash
# 정적 분석
npm install -D eslint-plugin-security

# 의존성 검사
npm audit
snyk test

# 헤더 보안
npm install helmet

# Rate Limiting
npm install express-rate-limit

# CSRF 방어
npm install csurf

# 입력 검증
npm install zod
npm install validator

# 암호화
npm install bcrypt

마무리 원칙

"보안은 선택이 아닌 필수다"

  • 의심하라: 모든 입력은 악의적이다
  • 최소화하라: 필요한 정보만 수집한다
  • 감춰라: 내부 구조를 노출하지 않는다
  • 기록하라: 모든 중요 이벤트를 로깅한다
  • 업데이트하라: 항상 최신 보안 패치를 적용한다

보안은 한 번에 완성되지 않습니다

  • 지속적인 모니터링
  • 정기적인 보안 점검
  • 팀원 교육
  • 최신 위협 동향 파악