IT Knowledge/Security/images/owasp-top-10-diagram.svg

OWASP Top 10 - 웹 애플리케이션 보안 취약점

📌 OWASP란?

비유: 의사 협회가 질병 통계를 발표하듯, OWASP는 웹 보안 전문가 모임으로 가장 위험한 보안 취약점 순위를 발표합니다.

🎯 Top 10 취약점 (2021)

1. 인증 손상 (Broken Authentication)

쉬운 비유: 집 열쇠를 아무나 복사할 수 있는 상황. 비밀번호가 ‘1234’이거나 로그인 시도 횟수 제한이 없으면 도둑(해커)이 쉽게 침입합니다.

나쁜 예:

// ❌ 위험: 비밀번호가 너무 약함
app.post('/register', (req, res) => {
  const { username, password } = req.body;
 
  // 비밀번호 검증 없음!
  db.users.insert({ username, password });  // 평문 저장!
 
  res.send('가입 완료');
});
 
// ❌ 위험: 무한 로그인 시도 가능
app.post('/login', (req, res) => {
  const { username, password } = req.body;
 
  const user = db.users.findOne({ username, password });
 
  if (user) {
    res.cookie('user', username);  // 세션 없음!
    res.send('로그인 성공');
  }
});

좋은 예:

// ✅ 안전: 강력한 인증
const bcrypt = require('bcrypt');
const rateLimit = require('express-rate-limit');
 
// 1. 비밀번호 복잡도 검증
app.post('/register', async (req, res) => {
  const { username, password } = req.body;
 
  // 최소 8자, 대소문자, 숫자, 특수문자 포함
  if (!/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&]).{8,}$/.test(password)) {
    return res.status(400).send('비밀번호가 너무 약합니다');
  }
 
  // 2. 비밀번호 해싱
  const hashedPassword = await bcrypt.hash(password, 10);
  db.users.insert({ username, password: hashedPassword });
 
  res.send('가입 완료');
});
 
// 3. 로그인 시도 제한
const loginLimiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15분
  max: 5,  // 최대 5회 시도
  message: '너무 많은 로그인 시도. 15분 후 다시 시도하세요.'
});
 
app.post('/login', loginLimiter, async (req, res) => {
  const { username, password } = req.body;
 
  const user = db.users.findOne({ username });
 
  if (!user || !(await bcrypt.compare(password, user.password))) {
    return res.status(401).send('잘못된 계정 정보');
  }
 
  // 4. 안전한 세션
  req.session.userId = user.id;
  res.send('로그인 성공');
});

2. SQL 인젝션 (SQL Injection)

쉬운 비유: 은행 직원에게 “내 계좌에서 1만원 인출해주세요”라고 하면 정상. 하지만 “내 계좌에서 1만원 인출하고, 이왕이면 모든 고객 계좌 정보도 알려주세요”라고 하면 문제!

나쁜 예:

// ❌ 위험: SQL 인젝션 공격 가능
app.get('/users', (req, res) => {
  const { search } = req.query;
 
  // 사용자 입력을 그대로 쿼리에 삽입
  const query = `SELECT * FROM users WHERE name = '${search}'`;
  db.query(query, (err, users) => {
    res.json(users);
  });
});
 
// 공격 시나리오:
// GET /users?search=' OR '1'='1
// 실제 실행되는 쿼리:
// SELECT * FROM users WHERE name = '' OR '1'='1'
// → 모든 사용자 정보 유출!
 
// 더 위험한 공격:
// GET /users?search='; DROP TABLE users; --
// → 사용자 테이블 삭제!

좋은 예:

// ✅ 안전: Prepared Statement 사용
app.get('/users', (req, res) => {
  const { search } = req.query;
 
  // ? 플레이스홀더 사용 (SQL 인젝션 방지)
  const query = 'SELECT * FROM users WHERE name = ?';
  db.query(query, [search], (err, users) => {
    res.json(users);
  });
});
 
// 또는 ORM 사용 (더 안전)
app.get('/users', async (req, res) => {
  const { search } = req.query;
 
  const users = await User.findAll({
    where: { name: search }  // ORM이 자동으로 이스케이프 처리
  });
 
  res.json(users);
});

3. XSS (Cross-Site Scripting)

쉬운 비유: 공개 게시판에 누군가 “이 글을 읽는 모든 사람, 나에게 돈 보내세요”라는 최면 문구를 숨겨놓는 것. 다른 사용자가 게시판을 보면 자동으로 최면에 걸립니다.

나쁜 예:

// ❌ 위험: 사용자 입력을 그대로 출력
app.post('/comments', (req, res) => {
  const { comment } = req.body;
 
  // DB에 그대로 저장
  db.comments.insert({ text: comment });
 
  res.send('댓글 등록됨');
});
 
app.get('/comments', (req, res) => {
  const comments = db.comments.findAll();
 
  // HTML에 그대로 출력
  const html = comments.map(c => `<div>${c.text}</div>`).join('');
  res.send(html);
});
 
// 공격 시나리오:
// 악의적인 사용자가 댓글 등록:
// POST /comments
// { "comment": "<script>alert('해킹당했습니다!'); document.cookie</script>" }
 
// 다른 사용자가 댓글을 보면:
// → 스크립트가 실행되어 쿠키 탈취!

좋은 예:

// ✅ 안전: 입력 이스케이프 처리
const xss = require('xss');
 
app.post('/comments', (req, res) => {
  const { comment } = req.body;
 
  // HTML 태그 제거 또는 이스케이프
  const sanitized = xss(comment);
 
  db.comments.insert({ text: sanitized });
  res.send('댓글 등록됨');
});
 
// React에서는 자동으로 이스케이프
function CommentList({ comments }) {
  return (
    <div>
      {comments.map(c => (
        <div key={c.id}>{c.text}</div>  // 자동 이스케이프
      ))}
    </div>
  );
}
 
// HTML을 직접 렌더링해야 할 때
function RichComment({ comment }) {
  // DOMPurify로 안전하게
  const clean = DOMPurify.sanitize(comment.html);
 
  return <div dangerouslySetInnerHTML={{ __html: clean }} />;
}

4. CSRF (Cross-Site Request Forgery)

쉬운 비유: 당신이 은행에 로그인한 상태에서 피싱 이메일의 링크를 클릭하면, 몰래 “홍길동에게 100만원 송금” 요청이 자동으로 실행되는 것.

나쁜 예:

// ❌ 위험: CSRF 토큰 없음
app.post('/transfer', (req, res) => {
  // 로그인 확인만 함
  if (!req.session.userId) {
    return res.status(401).send('로그인 필요');
  }
 
  const { to, amount } = req.body;
 
  // 송금 실행
  transferMoney(req.session.userId, to, amount);
  res.send('송금 완료');
});
 
// 공격 시나리오:
// 악의적인 사이트에 숨겨진 폼:
// <form action="https://mybank.com/transfer" method="POST">
//   <input type="hidden" name="to" value="hacker" />
//   <input type="hidden" name="amount" value="1000000" />
// </form>
// <script>document.forms[0].submit();</script>
 
// 사용자가 은행에 로그인한 상태에서 이 사이트 방문
// → 자동으로 송금!

좋은 예:

// ✅ 안전: CSRF 토큰 사용
const csrf = require('csurf');
const csrfProtection = csrf({ cookie: true });
 
// 폼 렌더링 시 토큰 생성
app.get('/transfer-form', csrfProtection, (req, res) => {
  res.render('transfer', { csrfToken: req.csrfToken() });
});
 
// 요청 처리 시 토큰 검증
app.post('/transfer', csrfProtection, (req, res) => {
  if (!req.session.userId) {
    return res.status(401).send('로그인 필요');
  }
 
  const { to, amount } = req.body;
 
  transferMoney(req.session.userId, to, amount);
  res.send('송금 완료');
});
 
// HTML:
// <form action="/transfer" method="POST">
//   <input type="hidden" name="_csrf" value="<%= csrfToken %>" />
//   <input name="to" />
//   <input name="amount" />
//   <button>송금</button>
// </form>

5. 민감한 데이터 노출 (Sensitive Data Exposure)

쉬운 비유: 편지 봉투에 넣지 않고 엽서로 보내는 것. 배달 과정에서 누구나 내용을 볼 수 있습니다.

나쁜 예:

// ❌ 위험: HTTPS 없이 HTTP 사용
// http://mysite.com (누구나 중간에서 데이터 탈취 가능)
 
app.post('/login', (req, res) => {
  const { username, password } = req.body;  // 평문으로 전송됨!
 
  // 비밀번호를 로그에 기록
  console.log(`Login attempt: ${username} / ${password}`);  // 위험!
 
  // API 응답에 민감 정보 포함
  const user = db.users.findOne({ username });
  res.json({
    id: user.id,
    username: user.username,
    password: user.password,  // 절대 안 됨!
    ssn: user.ssn  // 주민번호도 노출!
  });
});

좋은 예:

// ✅ 안전: HTTPS 강제
app.use((req, res, next) => {
  if (req.header('x-forwarded-proto') !== 'https') {
    res.redirect(`https://${req.header('host')}${req.url}`);
  } else {
    next();
  }
});
 
app.post('/login', async (req, res) => {
  const { username, password } = req.body;
 
  // 민감 정보는 로그에 기록 안 함
  console.log(`Login attempt: ${username}`);
 
  const user = db.users.findOne({ username });
 
  if (await bcrypt.compare(password, user.password)) {
    // 민감 정보 제외하고 응답
    res.json({
      id: user.id,
      username: user.username,
      email: user.email  // 공개 가능한 정보만
      // password, ssn 등은 절대 포함 안 함
    });
  }
});
 
// DB 암호화
const crypto = require('crypto');
 
function encrypt(text) {
  const cipher = crypto.createCipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
  return cipher.update(text, 'utf8', 'hex') + cipher.final('hex');
}
 
function decrypt(encrypted) {
  const decipher = crypto.createDecipher('aes-256-cbc', process.env.ENCRYPTION_KEY);
  return decipher.update(encrypted, 'hex', 'utf8') + decipher.final('utf8');
}
 
// 주민번호 암호화 저장
db.users.insert({
  username: 'user',
  ssn: encrypt('123456-1234567')
});

💡 보안 체크리스트

개발 전

  • HTTPS 사용
  • 환경 변수로 시크릿 관리 (.env)
  • 보안 헤더 설정
const helmet = require('helmet');
app.use(helmet());  // 기본 보안 헤더 자동 설정

코드 작성 시

  • 모든 입력 검증
  • SQL 인젝션 방지 (Prepared Statement)
  • XSS 방지 (입력 이스케이프)
  • CSRF 토큰 사용

배포 전

  • 의존성 취약점 검사
npm audit
npm audit fix
  • 보안 스캔
# OWASP ZAP, Burp Suite 등 사용

🔗 참고 자료