웹 애플리케이션의 보안을 한 단계 끌어올리는 실전 코딩 가이드입니다. 복잡한 보안 개념을 이해하기 쉬운 예제와 함께 설명드립니다.
모든 사용자 입력은 잠재적인 보안 위협으로 간주해야 합니다. 클라이언트 측 검증만으로는 충분하지 않으며, 서버 측에서 반드시 재검증해야 합니다.
// 위험한 코드 - 직접 데이터베이스 쿼리
const getUserData = async (userId) => {
const query = `SELECT * FROM users WHERE id = ${userId}`;
return await db.query(query);
};
// 안전한 코드 - 입력값 검증과 매개변수화된 쿼리
const getUserData = async (userId) => {
// 1. 입력값 타입 검증
if (!/^\d+$/.test(userId)) {
throw new Error('Invalid user ID format');
}
// 2. 매개변수화된 쿼리 사용
const query = 'SELECT * FROM users WHERE id = ?';
return await db.query(query, [userId]);
};
XSS 공격은 악성 스크립트를 웹 페이지에 주입하는 공격입니다. 사용자가 입력한 데이터를 그대로 화면에 출력할 때 발생할 수 있습니다.
import DOMPurify from 'dompurify';
// 위험한 방법 - dangerouslySetInnerHTML 직접 사용
const UnsafeComponent = ({ content }) => (
<div dangerouslySetInnerHTML={{__html: content}} />
);
// 안전한 방법 - DOMPurify로 정화
const SafeComponent = ({ content }) => {
const sanitizedContent = DOMPurify.sanitize(content);
return (
<div dangerouslySetInnerHTML={{__html: sanitizedContent}} />
);
};
// Express.js에서 CSP 헤더 설정
const helmet = require('helmet');
app.use(helmet({
contentSecurityPolicy: {
directives: {
'default-src': ["'self'"],
'script-src': ["'self'", "'unsafe-inline'"],
'style-src': ["'self'", "'unsafe-inline'"],
'img-src': ["'self'", 'data:', 'https:'],
}
}
}));
CORS는 다른 도메인에서 리소스에 접근할 때의 보안 정책입니다. 올바른 설정으로 필요한 접근은 허용하고, 악의적인 접근은 차단해야 합니다.
const cors = require('cors');
// 위험한 설정 - 모든 도메인 허용
app.use(cors({
origin: '*' // 절대 사용하지 마세요!
}));
// 안전한 설정 - 화이트리스트 기반
const allowedOrigins = [
'https://www.myapp.com',
'https://admin.myapp.com',
'http://localhost:3000' // 개발 환경용
];
app.use(cors({
origin: function (origin, callback) {
if (!origin || allowedOrigins.includes(origin)) {
callback(null, true);
} else {
callback(new Error('Not allowed by CORS'));
}
},
credentials: true, // 쿠키 전송 허용
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
}));
JWT(JSON Web Token)를 사용한 인증은 확장성이 좋지만, 적절한 보안 조치 없이는 오히려 취약점이 될 수 있습니다.
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
// 토큰 생성 시 짧은 만료 시간 설정
const generateTokens = (userId) => {
const accessToken = jwt.sign(
{ userId, type: 'access' },
process.env.JWT_SECRET,
{ expiresIn: '15m' } // 15분
);
const refreshToken = jwt.sign(
{ userId, type: 'refresh' },
process.env.REFRESH_SECRET,
{ expiresIn: '7d' } // 7일
);
return { accessToken, refreshToken };
};
// 토큰 검증 미들웨어
const verifyToken = (req, res, next) => {
const token = req.headers.authorization?.split(' ')[1];
if (!token) {
return res.status(401).json({ error: 'Access token required' });
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET);
req.userId = decoded.userId;
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
};