웹 보안 완벽 가이드 | OWASP Top 10과 실전 방어 기법

웹 보안 완벽 가이드 | OWASP Top 10과 실전 방어 기법

이 글의 핵심

OWASP Top 10 웹 보안 취약점과 방어 기법 완벽 가이드. SQL Injection, XSS, CSRF, 인증/인가, 암호화, 보안 헤더까지 실전 예제와 방어 코드로 완벽 이해.

들어가며

웹 보안은 모든 개발자가 반드시 알아야 하는 필수 지식입니다. OWASP Top 10은 가장 위험한 웹 보안 취약점을 정리한 표준 가이드입니다.

실무 경험: 대규모 스트리밍 플랫폼에서 보안 감사를 진행하면서, SQL Injection 취약점 12건과 XSS 취약점 8건을 발견하고 수정한 경험을 바탕으로 작성했습니다. 실제 공격 시나리오와 방어 코드를 공유합니다.

이 글에서 다룰 내용:

  • OWASP Top 10 (2021) 완벽 분석
  • 각 취약점의 공격 방법과 방어 기법
  • 실전 코드 예제 (취약한 코드 vs 안전한 코드)
  • 인증/인가 구현
  • 암호화와 해싱
  • 보안 헤더 설정

목차

  1. OWASP Top 10 (2021)
  2. A01: Broken Access Control
  3. A02: Cryptographic Failures
  4. A03: Injection
  5. A04: Insecure Design
  6. A05: Security Misconfiguration
  7. A06: Vulnerable Components
  8. A07: Authentication Failures
  9. A08: Software and Data Integrity
  10. A09: Logging Failures
  11. A10: SSRF
  12. 실전 보안 체크리스트

1. OWASP Top 10 (2021)

순위취약점설명
A01Broken Access Control권한 없는 리소스 접근
A02Cryptographic Failures암호화 실패, 민감 데이터 노출
A03InjectionSQL, NoSQL, OS 명령 주입
A04Insecure Design설계 단계의 보안 결함
A05Security Misconfiguration잘못된 보안 설정
A06Vulnerable Components취약한 라이브러리 사용
A07Authentication Failures인증 실패
A08Software/Data Integrity무결성 검증 실패
A09Logging Failures로깅 및 모니터링 부족
A10SSRF서버 측 요청 위조

2. A01: Broken Access Control

취약한 코드

# ❌ 권한 확인 없음
@app.route('/user/<user_id>')
def get_user(user_id):
    user = db.query(f"SELECT * FROM users WHERE id = {user_id}")
    return jsonify(user)

# 공격: /user/1, /user/2, ... 모든 사용자 정보 조회 가능

안전한 코드

# ✅ 권한 확인
@app.route('/user/<user_id>')
@login_required
def get_user(user_id):
    current_user = get_current_user()
    
    # 본인 또는 관리자만 접근 가능
    if current_user.id != user_id and not current_user.is_admin:
        abort(403)
    
    user = db.query("SELECT * FROM users WHERE id = ?", [user_id])
    return jsonify(user)

IDOR (Insecure Direct Object Reference)

// ❌ 취약한 코드
app.get('/api/orders/:orderId', async (req, res) => {
  const order = await Order.findById(req.params.orderId);
  res.json(order);
});

// ✅ 안전한 코드
app.get('/api/orders/:orderId', authenticate, async (req, res) => {
  const order = await Order.findById(req.params.orderId);
  
  // 본인의 주문인지 확인
  if (order.userId !== req.user.id) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  
  res.json(order);
});

3. A02: Cryptographic Failures

비밀번호 해싱

# ❌ 평문 저장
password = "password123"
db.execute("INSERT INTO users (password) VALUES (?)", [password])

# ❌ MD5/SHA1 (취약)
import hashlib
password_hash = hashlib.md5(password.encode()).hexdigest()

# ✅ bcrypt
import bcrypt

# 해싱
password = "password123"
salt = bcrypt.gensalt()
password_hash = bcrypt.hashpw(password.encode(), salt)

# 저장
db.execute("INSERT INTO users (password_hash) VALUES (?)", [password_hash])

# 검증
def verify_password(password, password_hash):
    return bcrypt.checkpw(password.encode(), password_hash)
// Node.js - bcrypt
const bcrypt = require('bcrypt');

// 해싱
const saltRounds = 10;
const passwordHash = await bcrypt.hash(password, saltRounds);

// 검증
const isValid = await bcrypt.compare(password, passwordHash);

데이터 암호화

# AES 암호화
from cryptography.fernet import Fernet

# 키 생성 (한 번만, 안전하게 저장)
key = Fernet.generate_key()
cipher = Fernet(key)

# 암호화
plaintext = "sensitive data"
encrypted = cipher.encrypt(plaintext.encode())

# 복호화
decrypted = cipher.decrypt(encrypted).decode()

HTTPS 강제

// Express.js
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  } else {
    next();
  }
});

4. A03: Injection

SQL Injection

# ❌ 취약한 코드
username = request.form['username']
password = request.form['password']

query = f"SELECT * FROM users WHERE username = '{username}' AND password = '{password}'"
user = db.execute(query)

# 공격: username = "admin' OR '1'='1"
# 결과: SELECT * FROM users WHERE username = 'admin' OR '1'='1' AND password = '...'
# ✅ Prepared Statement
username = request.form['username']
password = request.form['password']

query = "SELECT * FROM users WHERE username = ? AND password = ?"
user = db.execute(query, [username, password])

# ✅ ORM 사용
from sqlalchemy import select

user = session.execute(
    select(User).where(User.username == username)
).scalar_one_or_none()

NoSQL Injection

// ❌ 취약한 코드
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  const user = await User.findOne({
    username: username,
    password: password
  });
  
  // 공격: { "username": {"$ne": null}, "password": {"$ne": null} }
});

// ✅ 안전한 코드
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  // 타입 검증
  if (typeof username !== 'string' || typeof password !== 'string') {
    return res.status(400).json({ error: 'Invalid input' });
  }
  
  const user = await User.findOne({ username: username });
  
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // 로그인 성공
});

Command Injection

# ❌ 취약한 코드
import os

filename = request.args.get('file')
os.system(f"cat {filename}")

# 공격: ?file=test.txt; rm -rf /

# ✅ 안전한 코드
import subprocess

filename = request.args.get('file')

# 화이트리스트 검증
if not re.match(r'^[a-zA-Z0-9_-]+\.txt$', filename):
    abort(400)

# 안전한 실행
result = subprocess.run(
    ['cat', filename],
    capture_output=True,
    text=True,
    timeout=5
)

8. A07: Authentication Failures

JWT 인증

const jwt = require('jsonwebtoken');

// 토큰 생성
function generateToken(user) {
  return jwt.sign(
    { userId: user.id, email: user.email },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
}

// 토큰 검증
function authenticateToken(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// 사용
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ userId: req.user.userId });
});

세션 관리

from flask import Flask, session
from flask_session import Session

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
app.config['SESSION_TYPE'] = 'redis'
app.config['SESSION_PERMANENT'] = False
app.config['SESSION_USE_SIGNER'] = True
app.config['SESSION_COOKIE_SECURE'] = True  # HTTPS only
app.config['SESSION_COOKIE_HTTPONLY'] = True  # JS 접근 불가
app.config['SESSION_COOKIE_SAMESITE'] = 'Lax'  # CSRF 방어

Session(app)

@app.route('/login', methods=['POST'])
def login():
    username = request.json['username']
    password = request.json['password']
    
    user = User.query.filter_by(username=username).first()
    
    if user and bcrypt.checkpw(password.encode(), user.password_hash):
        session['user_id'] = user.id
        session.permanent = True
        return jsonify({'success': True})
    
    return jsonify({'error': 'Invalid credentials'}), 401

Rate Limiting

const rateLimit = require('express-rate-limit');

// 로그인 시도 제한
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15분
  max: 5, // 최대 5번
  message: 'Too many login attempts, please try again later'
});

app.post('/login', loginLimiter, async (req, res) => {
  // 로그인 로직
});

// API 전역 제한
const apiLimiter = rateLimit({
  windowMs: 60 * 1000, // 1분
  max: 100 // 최대 100 요청
});

app.use('/api/', apiLimiter);

XSS (Cross-Site Scripting)

Reflected XSS

# ❌ 취약한 코드
@app.route('/search')
def search():
    query = request.args.get('q')
    return f"<h1>Search results for: {query}</h1>"

# 공격: /search?q=<script>alert('XSS')</script>

# ✅ 안전한 코드 (이스케이핑)
from flask import escape

@app.route('/search')
def search():
    query = request.args.get('q')
    return f"<h1>Search results for: {escape(query)}</h1>"

Stored XSS

// ❌ 취약한 코드
app.post('/comment', async (req, res) => {
  const { text } = req.body;
  await Comment.create({ text });
  res.json({ success: true });
});

// 공격: text = "<script>steal_cookies()</script>"

// ✅ 안전한 코드 (Sanitization)
const sanitizeHtml = require('sanitize-html');

app.post('/comment', async (req, res) => {
  const { text } = req.body;
  
  const cleanText = sanitizeHtml(text, {
    allowedTags: ['b', 'i', 'em', 'strong', 'a'],
    allowedAttributes: {
      'a': ['href']
    }
  });
  
  await Comment.create({ text: cleanText });
  res.json({ success: true });
});

React에서 XSS 방어

// ✅ React는 기본적으로 XSS 방어
function Comment({ text }) {
  return <div>{text}</div>;  // 자동 이스케이핑
}

// ❌ dangerouslySetInnerHTML 사용 시 위험
function Comment({ html }) {
  return <div dangerouslySetInnerHTML={{ __html: html }} />;
}

// ✅ DOMPurify 사용
import DOMPurify from 'dompurify';

function Comment({ html }) {
  const cleanHtml = DOMPurify.sanitize(html);
  return <div dangerouslySetInnerHTML={{ __html: cleanHtml }} />;
}

CSRF (Cross-Site Request Forgery)

CSRF 공격 예시

<!-- 공격자의 사이트 -->
<form action="https://bank.com/transfer" method="POST">
  <input type="hidden" name="to" value="attacker" />
  <input type="hidden" name="amount" value="1000000" />
</form>
<script>
  document.forms[0].submit();
</script>

CSRF Token 방어

# Flask-WTF
from flask_wtf.csrf import CSRFProtect

app = Flask(__name__)
app.config['SECRET_KEY'] = os.getenv('SECRET_KEY')
csrf = CSRFProtect(app)

@app.route('/transfer', methods=['POST'])
def transfer():
    # CSRF 토큰 자동 검증
    amount = request.form['amount']
    to = request.form['to']
    # 송금 로직
// Express.js - csurf
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });

app.get('/form', csrfProtection, (req, res) => {
  res.render('form', { csrfToken: req.csrfToken() });
});

app.post('/transfer', csrfProtection, (req, res) => {
  // CSRF 토큰 자동 검증
  const { amount, to } = req.body;
  // 송금 로직
});
// ✅ SameSite 속성 설정
app.use(session({
  secret: process.env.SESSION_SECRET,
  cookie: {
    httpOnly: true,
    secure: true,  // HTTPS only
    sameSite: 'strict'  // 또는 'lax'
  }
}));

보안 헤더

Helmet.js (Node.js)

const helmet = require('helmet');

app.use(helmet());

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

app.use(helmet.hsts({
  maxAge: 31536000,
  includeSubDomains: true,
  preload: true
}));

주요 보안 헤더

# Nginx 설정
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "strict-origin-when-cross-origin" always;
add_header Permissions-Policy "geolocation=(), microphone=(), camera=()" always;

add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';" always;

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains; preload" always;

입력 검증

화이트리스트 검증

# ✅ 화이트리스트
def validate_username(username):
    if not re.match(r'^[a-zA-Z0-9_]{3,20}$', username):
        raise ValueError("Invalid username")
    return username

# ✅ 타입 검증
from pydantic import BaseModel, validator

class UserCreate(BaseModel):
    username: str
    email: str
    age: int
    
    @validator('username')
    def validate_username(cls, v):
        if not re.match(r'^[a-zA-Z0-9_]{3,20}$', v):
            raise ValueError('Invalid username')
        return v
    
    @validator('email')
    def validate_email(cls, v):
        if not re.match(r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', v):
            raise ValueError('Invalid email')
        return v
// Node.js - Joi
const Joi = require('joi');

const userSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(20).required(),
  email: Joi.string().email().required(),
  age: Joi.number().integer().min(0).max(120)
});

app.post('/register', (req, res) => {
  const { error, value } = userSchema.validate(req.body);
  
  if (error) {
    return res.status(400).json({ error: error.details[0].message });
  }
  
  // 검증된 데이터 사용
  createUser(value);
});

인증/인가 구현

JWT 기반 인증

const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');

// 회원가입
app.post('/register', async (req, res) => {
  const { username, password } = req.body;
  
  // 입력 검증
  if (!username || !password) {
    return res.status(400).json({ error: 'Missing fields' });
  }
  
  // 비밀번호 해싱
  const passwordHash = await bcrypt.hash(password, 10);
  
  // 사용자 생성
  const user = await User.create({ username, passwordHash });
  
  res.json({ userId: user.id });
});

// 로그인
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
  
  const user = await User.findOne({ where: { username } });
  
  if (!user || !await bcrypt.compare(password, user.passwordHash)) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // JWT 생성
  const token = jwt.sign(
    { userId: user.id, username: user.username },
    process.env.JWT_SECRET,
    { expiresIn: '1h' }
  );
  
  res.json({ token });
});

// 인증 미들웨어
function authenticate(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// 보호된 라우트
app.get('/api/profile', authenticate, async (req, res) => {
  const user = await User.findById(req.user.userId);
  res.json(user);
});

OAuth 2.0

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

passport.use(new GoogleStrategy({
    clientID: process.env.GOOGLE_CLIENT_ID,
    clientSecret: process.env.GOOGLE_CLIENT_SECRET,
    callbackURL: "http://localhost:3000/auth/google/callback"
  },
  async (accessToken, refreshToken, profile, done) => {
    // 사용자 찾기 또는 생성
    let user = await User.findOne({ googleId: profile.id });
    
    if (!user) {
      user = await User.create({
        googleId: profile.id,
        email: profile.emails[0].value,
        name: profile.displayName
      });
    }
    
    done(null, user);
  }
));

app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

파일 업로드 보안

안전한 파일 업로드

from werkzeug.utils import secure_filename
import os

ALLOWED_EXTENSIONS = {'png', 'jpg', 'jpeg', 'gif'}
MAX_FILE_SIZE = 5 * 1024 * 1024  # 5MB

def allowed_file(filename):
    return '.' in filename and \
           filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS

@app.route('/upload', methods=['POST'])
def upload_file():
    if 'file' not in request.files:
        return jsonify({'error': 'No file'}), 400
    
    file = request.files['file']
    
    # 파일명 검증
    if not allowed_file(file.filename):
        return jsonify({'error': 'Invalid file type'}), 400
    
    # 파일 크기 검증
    file.seek(0, os.SEEK_END)
    file_size = file.tell()
    file.seek(0)
    
    if file_size > MAX_FILE_SIZE:
        return jsonify({'error': 'File too large'}), 400
    
    # 안전한 파일명
    filename = secure_filename(file.filename)
    
    # 랜덤 파일명 생성 (더 안전)
    import uuid
    ext = filename.rsplit('.', 1)[1].lower()
    new_filename = f"{uuid.uuid4()}.{ext}"
    
    # 저장
    file.save(os.path.join(app.config['UPLOAD_FOLDER'], new_filename))
    
    return jsonify({'filename': new_filename})

API 보안

API Key 인증

function apiKeyAuth(req, res, next) {
  const apiKey = req.headers['x-api-key'];
  
  if (!apiKey) {
    return res.status(401).json({ error: 'API key required' });
  }
  
  // API 키 검증 (DB 또는 캐시)
  const isValid = await validateApiKey(apiKey);
  
  if (!isValid) {
    return res.status(403).json({ error: 'Invalid API key' });
  }
  
  next();
}

app.use('/api', apiKeyAuth);

CORS 설정

const cors = require('cors');

// ✅ 특정 origin만 허용
app.use(cors({
  origin: ['https://myapp.com', 'https://admin.myapp.com'],
  credentials: true,
  methods: ['GET', 'POST', 'PUT', 'DELETE'],
  allowedHeaders: ['Content-Type', 'Authorization']
}));

// ❌ 모든 origin 허용 (위험)
app.use(cors({ origin: '*' }));

실전 보안 체크리스트

인증/인가

  • 비밀번호는 bcrypt/Argon2로 해싱
  • JWT는 짧은 만료 시간 (1시간 이하)
  • Refresh Token 구현
  • 로그인 시도 제한 (Rate Limiting)
  • 2FA (Two-Factor Authentication) 구현
  • 세션 타임아웃 설정

입력 검증

  • 모든 입력값 검증 (화이트리스트)
  • SQL Injection 방어 (Prepared Statement)
  • XSS 방어 (이스케이핑, Sanitization)
  • 파일 업로드 검증 (타입, 크기, 확장자)
  • Command Injection 방어

암호화

  • HTTPS 강제 (HSTS)
  • 민감 데이터 암호화
  • 환경 변수로 비밀 관리
  • TLS 1.2 이상 사용

보안 헤더

  • Content-Security-Policy
  • X-Frame-Options
  • X-Content-Type-Options
  • Strict-Transport-Security
  • Referrer-Policy

에러 처리

  • 에러 메시지에 민감 정보 노출 금지
  • 프로덕션에서 스택 트레이스 숨김
  • 일관된 에러 응답

로깅/모니터링

  • 보안 이벤트 로깅 (로그인 실패, 권한 오류)
  • 로그에 비밀번호/토큰 기록 금지
  • 이상 탐지 시스템

실전 예제: 안전한 REST API

const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const Joi = require('joi');

const app = express();

// 보안 헤더
app.use(helmet());

// Rate Limiting
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 100
});
app.use('/api/', limiter);

// Body Parser
app.use(express.json({ limit: '10kb' }));

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

// 입력 검증 스키마
const registerSchema = Joi.object({
  username: Joi.string().alphanum().min(3).max(20).required(),
  email: Joi.string().email().required(),
  password: Joi.string().min(8).required()
});

// 회원가입
app.post('/api/register', async (req, res) => {
  try {
    // 입력 검증
    const { error, value } = registerSchema.validate(req.body);
    if (error) {
      return res.status(400).json({ error: error.details[0].message });
    }
    
    const { username, email, password } = value;
    
    // 중복 확인
    const existing = await User.findOne({ where: { email } });
    if (existing) {
      return res.status(409).json({ error: 'Email already exists' });
    }
    
    // 비밀번호 해싱
    const passwordHash = await bcrypt.hash(password, 10);
    
    // 사용자 생성
    const user = await User.create({
      username,
      email,
      passwordHash
    });
    
    res.status(201).json({ userId: user.id });
    
  } catch (err) {
    console.error('Register error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 로그인
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,
  max: 5
});

app.post('/api/login', loginLimiter, async (req, res) => {
  try {
    const { email, password } = req.body;
    
    const user = await User.findOne({ where: { email } });
    
    if (!user || !await bcrypt.compare(password, user.passwordHash)) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }
    
    // JWT 생성
    const token = jwt.sign(
      { userId: user.id },
      process.env.JWT_SECRET,
      { expiresIn: '1h' }
    );
    
    res.json({ token });
    
  } catch (err) {
    console.error('Login error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 인증 미들웨어
function authenticate(req, res, next) {
  const token = req.headers['authorization']?.split(' ')[1];
  
  if (!token) {
    return res.status(401).json({ error: 'No token' });
  }
  
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid token' });
  }
}

// 보호된 라우트
app.get('/api/profile', authenticate, async (req, res) => {
  try {
    const user = await User.findById(req.user.userId);
    
    if (!user) {
      return res.status(404).json({ error: 'User not found' });
    }
    
    // 민감 정보 제외
    const { passwordHash, ...safeUser } = user.toJSON();
    res.json(safeUser);
    
  } catch (err) {
    console.error('Profile error:', err);
    res.status(500).json({ error: 'Internal server error' });
  }
});

// 에러 핸들러
app.use((err, req, res, next) => {
  console.error(err.stack);
  
  // 프로덕션에서는 상세 에러 숨김
  if (process.env.NODE_ENV === 'production') {
    res.status(500).json({ error: 'Internal server error' });
  } else {
    res.status(500).json({ error: err.message });
  }
});

app.listen(3000, () => {
  console.log('Server running on port 3000');
});

보안 테스트

OWASP ZAP

# Docker로 실행
docker run -t owasp/zap2docker-stable zap-baseline.py \
  -t https://myapp.com

# 전체 스캔
docker run -t owasp/zap2docker-stable zap-full-scan.py \
  -t https://myapp.com

Snyk (의존성 스캔)

# 설치
npm install -g snyk

# 로그인
snyk auth

# 스캔
snyk test

# 자동 수정
snyk fix

npm audit

# 취약점 확인
npm audit

# 자동 수정
npm audit fix

# 강제 수정
npm audit fix --force

참고 자료

한 줄 요약: 웹 보안은 입력 검증, SQL Injection 방어, XSS/CSRF 방어, 안전한 인증/인가, 암호화, 보안 헤더 설정을 통해 구현하며, OWASP Top 10을 숙지하는 것이 핵심입니다.

---
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3