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 등 사용